9 Commits

Author SHA1 Message Date
Onur Cayan c8d6314e48 Add source-aware UART log replay
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-08-12 13:54:06 +03:00
Onur Cayan f7fa2b3e4d Fix incomplete map tile updates
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-08-10 00:51:00 +03:00
onur 745c7d91af Normalize zero-based firmware satellite indices
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-07-24 12:06:12 +03:00
onur db96fd4da2 Verify satellite propagation is timezone invariant 2026-07-24 11:58:18 +03:00
onur 140b0e35b5 Show RINEX status time in local timezone 2026-07-24 11:51:12 +03:00
onur 73227452c3 Merge GUI updates into macOS branch
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
# Conflicts:
#	src/GlobeWidget.cpp
#	src/GlobeWidget.h
#	src/MainWindow.h
#	src/MapWidget.cpp
#	src/MapWidget.h
#	src/RinexNavigation.cpp
2026-07-24 00:14:47 +03:00
onur f012b7a52a Lock map tracking to corrected IMU output
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-07-18 18:06:02 +03:00
onur 7a883b801f Add native macOS settings and device integration
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-07-18 16:51:50 +03:00
onur e648eca324 Add polished macOS location plotter experience
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
2026-07-18 15:20:37 +03:00
12 changed files with 882 additions and 93 deletions
+16 -2
View File
@@ -48,7 +48,7 @@ endif()
qt_add_resources(location_plotter "app_resources" qt_add_resources(location_plotter "app_resources"
PREFIX "/" 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) if(UNIX AND NOT APPLE)
@@ -72,6 +72,20 @@ target_compile_options(location_parser_tests PRIVATE
) )
add_test(NAME location_parser_tests COMMAND location_parser_tests) add_test(NAME location_parser_tests COMMAND location_parser_tests)
add_executable(location_map_widget_tests
tests/map_widget_tests.cpp
src/MapWidget.cpp
src/MapWidget.h
src/TelemetryPoint.h
)
target_link_libraries(location_map_widget_tests PRIVATE Qt6::Core Qt6::Widgets Qt6::Network)
target_compile_options(location_map_widget_tests PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-UNDEBUG>
)
add_test(NAME location_map_widget_tests COMMAND location_map_widget_tests)
set_tests_properties(location_map_widget_tests PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;XDG_CACHE_HOME=/tmp/location-plotter-map-test-cache")
add_executable(location_main_window_tests add_executable(location_main_window_tests
tests/main_window_tests.cpp tests/main_window_tests.cpp
src/LocationParser.cpp src/LocationParser.cpp
@@ -100,7 +114,7 @@ target_compile_options(location_main_window_tests PRIVATE
) )
qt_add_resources(location_main_window_tests "test_app_resources" qt_add_resources(location_main_window_tests "test_app_resources"
PREFIX "/" 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) if(UNIX AND NOT APPLE)
target_compile_definitions(location_main_window_tests PRIVATE LOCATION_PLOTTER_LINUX_SERIAL=1) target_compile_definitions(location_main_window_tests PRIVATE LOCATION_PLOTTER_LINUX_SERIAL=1)
+2
View File
@@ -232,6 +232,8 @@ without logging out.
(10 degrees by default, press Enter after typing); (10 degrees by default, press Enter after typing);
satellites inside the mask get a cyan sight line and halo. Incoming positions update the 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. 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 - 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 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 10 FPS. positions are propagated from the cached ephemeris, then smoothly interpolated at 10 FPS.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+288 -22
View File
@@ -1,12 +1,14 @@
#include "GlobeWidget.h" #include "GlobeWidget.h"
#include <QLinearGradient> #include <QLinearGradient>
#include <QDateTime>
#include <QHash> #include <QHash>
#include <QFile> #include <QFile>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QMouseEvent> #include <QMouseEvent>
#include <QNativeGestureEvent>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QPolygonF> #include <QPolygonF>
@@ -185,6 +187,15 @@ const QImage &landMask()
return mask; 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) bool maskIsLand(double longitude, double latitude)
{ {
const QImage &mask = landMask(); const QImage &mask = landMask();
@@ -354,14 +365,18 @@ void GlobeWidget::paintEvent(QPaintEvent *)
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
const qreal devicePixelRatio = devicePixelRatioF(); const qreal devicePixelRatio = devicePixelRatioF();
const qint64 lightingMinute =
QDateTime::currentDateTimeUtc().toSecsSinceEpoch() / 60;
if (m_staticLayer.isNull() || m_staticLayerSize != size() if (m_staticLayer.isNull() || m_staticLayerSize != size()
|| !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio) || !qFuzzyCompare(m_staticLayerDevicePixelRatio, devicePixelRatio)
|| m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch || m_staticLayerYaw != m_yaw || m_staticLayerPitch != m_pitch
|| m_staticLayerZoom != m_zoom) { || m_staticLayerZoom != m_zoom
|| m_staticLayerLightingMinute != lightingMinute) {
rebuildStaticLayer(); rebuildStaticLayer();
} }
painter.drawPixmap(0, 0, m_staticLayer); painter.drawPixmap(0, 0, m_staticLayer);
drawSatelliteCoverage(painter);
if (m_hasReceiver) drawVisibilityCone(painter); if (m_hasReceiver) drawVisibilityCone(painter);
if (m_hasReceiver) drawReceiver(painter); if (m_hasReceiver) drawReceiver(painter);
const int visibleCount = drawSatellites(painter); const int visibleCount = drawSatellites(painter);
@@ -454,10 +469,26 @@ void GlobeWidget::rebuildStaticLayer()
QPainterPath sphereClip; QPainterPath sphereClip;
sphereClip.addEllipse(center, radius, radius); sphereClip.addEllipse(center, radius, radius);
painter.setClipPath(sphereClip); painter.setClipPath(sphereClip);
drawContinents(painter); drawBlueMarble(painter);
drawGraticule(painter); drawGraticule(painter);
painter.restore(); 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); for (int i = 0; i < 3; ++i) drawOrbit(painter, 55.0 + i * 1.8, i * 61.0 + 12.0);
painter.end(); painter.end();
@@ -467,6 +498,107 @@ void GlobeWidget::rebuildStaticLayer()
m_staticLayerYaw = m_yaw; m_staticLayerYaw = m_yaw;
m_staticLayerPitch = m_pitch; m_staticLayerPitch = m_pitch;
m_staticLayerZoom = m_zoom; m_staticLayerZoom = m_zoom;
m_staticLayerLightingMinute =
QDateTime::currentDateTimeUtc().toSecsSinceEpoch() / 60;
}
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;
// Interactive rotation favors latency; mouse release immediately causes
// a full Retina-quality rebuild. Satellite animation frames reuse the
// cached texture and therefore do not repeat this projection work.
const double pixelRatio = m_dragging ? std::min(devicePixelRatioF(), 1.0)
: std::min(devicePixelRatioF(), 2.0);
const int textureSize = m_dragging
? std::clamp(static_cast<int>(std::ceil(radius * 2.0 * pixelRatio)), 280, 650)
: 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 void GlobeWidget::drawContinents(QPainter &painter) const
@@ -479,8 +611,11 @@ void GlobeWidget::drawContinents(QPainter &painter) const
const QPointF center(width() * 0.5, height() * 0.52); const QPointF center(width() * 0.5, height() * 0.52);
const QRectF target(center.x() - radius, center.y() - radius, const QRectF target(center.x() - radius, center.y() - radius,
radius * 2.0, radius * 2.0); 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 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.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(target, m_landTexture); painter.drawImage(target, m_landTexture);
return; return;
@@ -492,6 +627,18 @@ void GlobeWidget::drawContinents(QPainter &painter) const
const double sy = std::sin(m_yaw); const double sy = std::sin(m_yaw);
const double cp = std::cos(m_pitch); const double cp = std::cos(m_pitch);
const double sp = std::sin(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) { for (int py = 0; py < textureSize; ++py) {
QRgb *scanLine = reinterpret_cast<QRgb *>(land.scanLine(py)); QRgb *scanLine = reinterpret_cast<QRgb *>(land.scanLine(py));
const double screenZ = -((py + 0.5) / textureSize * 2.0 - 1.0); const double screenZ = -((py + 0.5) / textureSize * 2.0 - 1.0);
@@ -512,16 +659,37 @@ void GlobeWidget::drawContinents(QPainter &painter) const
|| maskIsLand(longitude - 0.55, latitude) || maskIsLand(longitude - 0.55, latitude)
|| maskIsLand(longitude, latitude + 0.55) || maskIsLand(longitude, latitude + 0.55)
|| 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 (!onLand) {
if (nearLand) { const double oceanPattern = 0.5 + 0.25 * std::sin(longitude * Pi / 31.0)
const int alpha = static_cast<int>(65.0 * std::clamp(depth * 1.5, 0.0, 1.0)); + 0.18 * std::cos(latitude * Pi / 17.0 + longitude * Pi / 47.0);
scanLine[px] = qRgba(33, 151, 174, alpha); 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; 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 lon = longitude * Pi / 180.0;
const double lat = latitude * 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) const double broadRelief = 0.50 + 0.24 * std::sin(lon * 5.0 + lat * 2.2)
@@ -550,31 +718,110 @@ void GlobeWidget::drawContinents(QPainter &painter) const
red = red * (1.0 - snow) + 224.0 * snow; red = red * (1.0 - snow) + 224.0 * snow;
green = green * (1.0 - snow) + 231.0 * snow; green = green * (1.0 - snow) + 231.0 * snow;
blue = blue * (1.0 - snow) + 224.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) const bool coast = !maskIsLand(longitude + 0.55, latitude)
|| !maskIsLand(longitude - 0.55, latitude) || !maskIsLand(longitude - 0.55, latitude)
|| !maskIsLand(longitude, latitude + 0.55) || !maskIsLand(longitude, latitude + 0.55)
|| !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) { if (coast) {
red += 28.0; red += 28.0;
green += 31.0; green += 31.0;
blue += 18.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( scanLine[px] = qRgba(
std::clamp(static_cast<int>(red * shade), 0, 255), std::clamp(static_cast<int>(red * shade + cityLights * 235.0), 0, 255),
std::clamp(static_cast<int>(green * shade), 0, 255), std::clamp(static_cast<int>(green * shade + cityLights * 155.0), 0, 255),
std::clamp(static_cast<int>(blue * shade), 0, 255), 238); std::clamp(static_cast<int>(blue * shade + cityLights * 52.0), 0, 255), 248);
} }
} }
m_landTexture = land; m_landTexture = land;
m_landTextureYaw = m_yaw; m_landTextureYaw = m_yaw;
m_landTexturePitch = m_pitch; m_landTexturePitch = m_pitch;
m_landTextureLightingMinute = lightingMinute;
painter.setRenderHint(QPainter::SmoothPixmapTransform, true); painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(target, m_landTexture); 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 void GlobeWidget::drawStars(QPainter &painter) const
{ {
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
@@ -591,6 +838,10 @@ void GlobeWidget::drawStars(QPainter &painter) const
void GlobeWidget::drawGraticule(QPainter &painter) const void GlobeWidget::drawGraticule(QPainter &painter) const
{ {
const double radius = std::min(width(), height()) * 0.255 * m_zoom; 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)); painter.setPen(QPen(QColor(100, 193, 221, 48), 0.8));
for (int latitude = -60; latitude <= 60; latitude += 30) { for (int latitude = -60; latitude <= 60; latitude += 30) {
QPainterPath path; QPainterPath path;
@@ -707,14 +958,10 @@ void GlobeWidget::drawVisibilityCone(QPainter &painter) const
earthRadiusPixels * displayOrbitRatio)); earthRadiusPixels * displayOrbitRatio));
} }
painter.setPen(Qt::NoPen); // Do not fill the projected cone with a triangle fan. When part of the
for (int i = 0; i < Segments; ++i) { // ring is behind the globe that fan self-intersects and produces dark
const double averageDepth = (apex.depth + ring[i].depth + ring[i + 1].depth) / 3.0; // rectangular/polygonal artifacts over the Earth texture. The boundary,
const int alpha = averageDepth >= 0.0 ? 18 : 7; // spokes and sight lines communicate the same mask without obscuring it.
painter.setBrush(QColor(48, 207, 218, alpha));
painter.drawPolygon(QPolygonF{apex.point, ring[i].point, ring[i + 1].point});
}
QPainterPath boundary; QPainterPath boundary;
boundary.moveTo(ring.first().point); boundary.moveTo(ring.first().point);
for (int i = 1; i < ring.size(); ++i) boundary.lineTo(ring[i].point); for (int i = 1; i < ring.size(); ++i) boundary.lineTo(ring[i].point);
@@ -864,7 +1111,26 @@ void GlobeWidget::mouseReleaseEvent(QMouseEvent *event)
void GlobeWidget::wheelEvent(QWheelEvent *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(); update();
event->accept(); 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);
}
+5
View File
@@ -30,6 +30,7 @@ public slots:
void setElevationMask(double degrees); void setElevationMask(double degrees);
protected: protected:
bool event(QEvent *event) override;
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override;
@@ -46,6 +47,8 @@ private:
void drawStars(QPainter &painter) const; void drawStars(QPainter &painter) const;
void drawOrbit(QPainter &painter, double inclination, double node) const; void drawOrbit(QPainter &painter, double inclination, double node) const;
void drawContinents(QPainter &painter) const; void drawContinents(QPainter &painter) const;
void drawBlueMarble(QPainter &painter) const;
void drawSatelliteCoverage(QPainter &painter) const;
void drawVisibilityCone(QPainter &painter) const; void drawVisibilityCone(QPainter &painter) const;
void drawGraticule(QPainter &painter) const; void drawGraticule(QPainter &painter) const;
void drawReceiver(QPainter &painter) const; void drawReceiver(QPainter &painter) const;
@@ -63,12 +66,14 @@ private:
mutable QImage m_landTexture; mutable QImage m_landTexture;
mutable double m_landTextureYaw = 0.0; mutable double m_landTextureYaw = 0.0;
mutable double m_landTexturePitch = 0.0; mutable double m_landTexturePitch = 0.0;
mutable qint64 m_landTextureLightingMinute = -1;
QPixmap m_staticLayer; QPixmap m_staticLayer;
QSize m_staticLayerSize; QSize m_staticLayerSize;
qreal m_staticLayerDevicePixelRatio = 0.0; qreal m_staticLayerDevicePixelRatio = 0.0;
double m_staticLayerYaw = 0.0; double m_staticLayerYaw = 0.0;
double m_staticLayerPitch = 0.0; double m_staticLayerPitch = 0.0;
double m_staticLayerZoom = 0.0; double m_staticLayerZoom = 0.0;
qint64 m_staticLayerLightingMinute = -1;
QSet<QString> m_observedPrns; QSet<QString> m_observedPrns;
QString m_status = QStringLiteral("Konum bekleniyor"); QString m_status = QStringLiteral("Konum bekleniyor");
bool m_statusError = false; bool m_statusError = false;
+160 -10
View File
@@ -26,14 +26,15 @@
#include <QHeaderView> #include <QHeaderView>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMenuBar>
#include <QMessageBox> #include <QMessageBox>
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QScrollArea> #include <QScrollArea>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSettings>
#include <QSpinBox> #include <QSpinBox>
#include <QSplitter> #include <QSplitter>
#include <QSettings>
#include <QStatusBar> #include <QStatusBar>
#include <QStyle> #include <QStyle>
#include <QTabWidget> #include <QTabWidget>
@@ -167,6 +168,7 @@ LocationAnalysis analyzeLocations(const QVector<TelemetryPoint> &points)
QVector<TelemetryPoint> preferredAnalysisPoints(const QVector<TelemetryPoint> &points) QVector<TelemetryPoint> preferredAnalysisPoints(const QVector<TelemetryPoint> &points)
{ {
QVector<TelemetryPoint> corrected; QVector<TelemetryPoint> corrected;
corrected.reserve(points.size());
for (const TelemetryPoint &point : points) { for (const TelemetryPoint &point : points) {
if (point.source == LocationSource::ImuCorrected) { if (point.source == LocationSource::ImuCorrected) {
corrected.push_back(point); corrected.push_back(point);
@@ -386,6 +388,7 @@ MainWindow::MainWindow(QWidget *parent)
, m_rinex(new RinexNavigation(this)) , m_rinex(new RinexNavigation(this))
{ {
buildUi(); buildUi();
buildMenus();
m_locationTimeoutTimer = new QTimer(this); m_locationTimeoutTimer = new QTimer(this);
m_locationTimeoutTimer->setSingleShot(true); m_locationTimeoutTimer->setSingleShot(true);
@@ -395,6 +398,9 @@ MainWindow::MainWindow(QWidget *parent)
m_uiRefreshTimer->setSingleShot(true); m_uiRefreshTimer->setSingleShot(true);
m_uiRefreshTimer->setInterval(UiRefreshIntervalMs); m_uiRefreshTimer->setInterval(UiRefreshIntervalMs);
m_portRefreshTimer = new QTimer(this);
m_portRefreshTimer->setInterval(2000);
connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine); connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine);
connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus); connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus);
connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError); connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError);
@@ -434,11 +440,21 @@ MainWindow::MainWindow(QWidget *parent)
}); });
connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout); connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout);
connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates); connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates);
connect(m_portRefreshTimer, &QTimer::timeout, this, [this] {
if (!m_serial->isOpen()) refreshSerialPorts();
});
m_portRefreshTimer->start();
connect(m_rinex, &RinexNavigation::satelliteMotionUpdated, connect(m_rinex, &RinexNavigation::satelliteMotionUpdated,
m_globe, &GlobeWidget::setSatelliteMotion); m_globe, &GlobeWidget::setSatelliteMotion);
connect(m_rinex, &RinexNavigation::statusChanged, this, connect(m_rinex, &RinexNavigation::statusChanged, this,
[this](const QString &status, bool error) { [this](const QString &status, bool error) {
m_globe->setRinexStatus(status, 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); statusBar()->showMessage(status, error ? 8000 : 4000);
}); });
connect(m_rinex, &RinexNavigation::loadingChanged, this, [this](bool loading) { connect(m_rinex, &RinexNavigation::loadingChanged, this, [this](bool loading) {
@@ -456,6 +472,7 @@ MainWindow::MainWindow(QWidget *parent)
rinexRefreshTimer->start(); rinexRefreshTimer->start();
resize(1480, 900); resize(1480, 900);
loadSettings();
setWindowTitle(QStringLiteral("Canli Konum Haritasi")); setWindowTitle(QStringLiteral("Canli Konum Haritasi"));
} }
@@ -463,6 +480,7 @@ void MainWindow::closeEvent(QCloseEvent *event)
{ {
m_replay->stop(); m_replay->stop();
stopUartLogging(); stopUartLogging();
saveSettings();
QMainWindow::closeEvent(event); QMainWindow::closeEvent(event);
} }
@@ -628,20 +646,19 @@ bool MainWindow::loadTelemetryLog(const QString &filePath)
resetTelemetryState(); resetTelemetryState();
const int replayCapacity = static_cast<int>(std::min<qsizetype>(10000, lines.size())); const int replayCapacity = static_cast<int>(std::min<qsizetype>(10000, lines.size()));
m_maxPointsSpin->setValue(std::max(10, replayCapacity)); m_maxPointsSpin->setValue(std::max(10, replayCapacity));
int parsedLocations = 0;
for (const QByteArray &rawLine : lines) { for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed(); const QString line = QString::fromUtf8(rawLine).trimmed();
if (line.isEmpty() || line.startsWith(QLatin1Char('#'))) { if (line.isEmpty() || line.startsWith(QLatin1Char('#'))) {
continue; continue;
} }
const auto telemetry = LocationParser::parseLine(line);
if (telemetry.has_value() && telemetry->hasLocation()) {
++parsedLocations;
}
handleSerialLine(line); 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(); flushUiUpdates();
m_map->fitToPoints(); m_map->fitToPoints();
const int parsedLocations = m_points.size();
const QString message = QStringLiteral("Replay yuklendi: %1 konum - %2") const QString message = QStringLiteral("Replay yuklendi: %1 konum - %2")
.arg(parsedLocations).arg(filePath); .arg(parsedLocations).arg(filePath);
m_statusLabel->setText(message); m_statusLabel->setText(message);
@@ -754,6 +771,14 @@ void MainWindow::refreshSerialPorts()
const QString configuredPort = qEnvironmentVariable("LOCATION_PLOTTER_PORT").trimmed(); const QString configuredPort = qEnvironmentVariable("LOCATION_PLOTTER_PORT").trimmed();
const QStringList detectedPorts = detectedSerialPorts(); const QStringList detectedPorts = detectedSerialPorts();
if (m_portsInitialized && detectedPorts == m_lastDetectedPorts) {
return;
}
const bool deviceAdded = m_portsInitialized
&& detectedPorts.size() > m_lastDetectedPorts.size();
m_lastDetectedPorts = detectedPorts;
m_portsInitialized = true;
m_portCombo->clear(); m_portCombo->clear();
for (const QString &port : detectedPorts) { for (const QString &port : detectedPorts) {
m_portCombo->addItem(port); m_portCombo->addItem(port);
@@ -774,6 +799,110 @@ void MainWindow::refreshSerialPorts()
m_portCombo->addItem(selectedPort); m_portCombo->addItem(selectedPort);
} }
m_portCombo->setCurrentText(selectedPort); m_portCombo->setCurrentText(selectedPort);
if (deviceAdded) {
statusBar()->showMessage(QStringLiteral("Yeni USB UART aygiti algilandi: %1")
.arg(selectedPort), 4000);
}
}
void MainWindow::buildMenus()
{
auto *fileMenu = menuBar()->addMenu(QStringLiteral("Dosya"));
QAction *openAction = fileMenu->addAction(QStringLiteral("Senaryo Logu Ac..."));
openAction->setObjectName(QStringLiteral("openLogAction"));
openAction->setShortcut(QKeySequence::Open);
connect(openAction, &QAction::triggered, this, &MainWindow::openTelemetryLog);
QAction *replayAction = fileMenu->addAction(QStringLiteral("UART Log Replay..."));
replayAction->setObjectName(QStringLiteral("uartReplayAction"));
replayAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+O")));
connect(replayAction, &QAction::triggered, this, &MainWindow::openUartReplay);
fileMenu->addSeparator();
QAction *quitAction = fileMenu->addAction(QStringLiteral("Cikis"));
quitAction->setShortcut(QKeySequence::Quit);
connect(quitAction, &QAction::triggered, this, &QWidget::close);
auto *connectionMenu = menuBar()->addMenu(QStringLiteral("Baglanti"));
QAction *listenAction = connectionMenu->addAction(QStringLiteral("Seri Baglantiyi Ac / Kapat"));
listenAction->setObjectName(QStringLiteral("toggleSerialAction"));
listenAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+K")));
connect(listenAction, &QAction::triggered, this, &MainWindow::toggleSerial);
QAction *portsAction = connectionMenu->addAction(QStringLiteral("Portlari Yenile"));
portsAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+R")));
connect(portsAction, &QAction::triggered, this, [this] {
m_portsInitialized = false;
refreshSerialPorts();
});
QAction *rinexAction = connectionMenu->addAction(QStringLiteral("RINEX Guncelle"));
rinexAction->setObjectName(QStringLiteral("rinexUpdateAction"));
rinexAction->setShortcut(QKeySequence::Refresh);
connect(rinexAction, &QAction::triggered, m_rinex, &RinexNavigation::refresh);
auto *viewMenu = menuBar()->addMenu(QStringLiteral("Gorunum"));
QAction *fitAction = viewMenu->addAction(QStringLiteral("Rotayi Haritaya Sigdir"));
fitAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+0")));
connect(fitAction, &QAction::triggered, m_map, &MapWidget::fitToPoints);
QAction *zoomInAction = viewMenu->addAction(QStringLiteral("Yaklastir"));
zoomInAction->setShortcut(QKeySequence::ZoomIn);
connect(zoomInAction, &QAction::triggered, m_map, &MapWidget::zoomIn);
QAction *zoomOutAction = viewMenu->addAction(QStringLiteral("Uzaklastir"));
zoomOutAction->setShortcut(QKeySequence::ZoomOut);
connect(zoomOutAction, &QAction::triggered, m_map, &MapWidget::zoomOut);
auto *helpMenu = menuBar()->addMenu(QStringLiteral("Yardim"));
QAction *aboutAction = helpMenu->addAction(QStringLiteral("Location Plotter Hakkinda"));
connect(aboutAction, &QAction::triggered, this, [this] {
QMessageBox::about(this, QStringLiteral("Location Plotter"),
QStringLiteral("GNSS telemetri, konum analizi ve RINEX uydu gorunumu\n"
"macOS surumu 0.2.0"));
});
}
void MainWindow::loadSettings()
{
QSettings settings;
restoreGeometry(settings.value(QStringLiteral("window/geometry")).toByteArray());
if (auto *splitter = findChild<QSplitter *>(QStringLiteral("mainSplitter")))
splitter->restoreState(settings.value(QStringLiteral("window/mainSplitter")).toByteArray());
if (auto *splitter = findChild<QSplitter *>(QStringLiteral("sideSplitter")))
splitter->restoreState(settings.value(QStringLiteral("window/sideSplitter")).toByteArray());
const QString port = settings.value(QStringLiteral("serial/port")).toString();
if (!port.isEmpty()) {
if (m_portCombo->findText(port) < 0) m_portCombo->addItem(port);
m_portCombo->setCurrentText(port);
}
m_baudCombo->setCurrentText(settings.value(QStringLiteral("serial/baud"), 115200).toString());
m_maxPointsSpin->setValue(settings.value(QStringLiteral("telemetry/maxPoints"), 10).toInt());
m_autoFollowCheck->setChecked(settings.value(QStringLiteral("map/autoFollow"), true).toBool());
m_showTelemetryCheck->setChecked(settings.value(QStringLiteral("map/showTelemetry"), true).toBool());
m_showRecentPointsCheck->setChecked(settings.value(QStringLiteral("map/showRecent"), true).toBool());
m_showAverageCheck->setChecked(settings.value(QStringLiteral("map/showAverage"), true).toBool());
if (auto *elevation = findChild<QDoubleSpinBox *>(QStringLiteral("elevationMaskSpin")))
elevation->setValue(settings.value(QStringLiteral("globe/elevationMask"), 10.0).toDouble());
if (auto *tabs = findChild<QTabWidget *>(QStringLiteral("mainViewTabs")))
tabs->setCurrentIndex(settings.value(QStringLiteral("window/mainTab"), 0).toInt());
}
void MainWindow::saveSettings() const
{
QSettings settings;
settings.setValue(QStringLiteral("window/geometry"), saveGeometry());
if (auto *splitter = findChild<QSplitter *>(QStringLiteral("mainSplitter")))
settings.setValue(QStringLiteral("window/mainSplitter"), splitter->saveState());
if (auto *splitter = findChild<QSplitter *>(QStringLiteral("sideSplitter")))
settings.setValue(QStringLiteral("window/sideSplitter"), splitter->saveState());
settings.setValue(QStringLiteral("serial/port"), m_portCombo->currentText().trimmed());
settings.setValue(QStringLiteral("serial/baud"), selectedBaudRate());
settings.setValue(QStringLiteral("telemetry/maxPoints"), m_maxPointsSpin->value());
settings.setValue(QStringLiteral("map/autoFollow"), m_autoFollowCheck->isChecked());
settings.setValue(QStringLiteral("map/showTelemetry"), m_showTelemetryCheck->isChecked());
settings.setValue(QStringLiteral("map/showRecent"), m_showRecentPointsCheck->isChecked());
settings.setValue(QStringLiteral("map/showAverage"), m_showAverageCheck->isChecked());
if (auto *elevation = findChild<QDoubleSpinBox *>(QStringLiteral("elevationMaskSpin")))
settings.setValue(QStringLiteral("globe/elevationMask"), elevation->value());
if (auto *tabs = findChild<QTabWidget *>(QStringLiteral("mainViewTabs")))
settings.setValue(QStringLiteral("window/mainTab"), tabs->currentIndex());
settings.sync();
} }
void MainWindow::handleSerialLine(const QString &line) void MainWindow::handleSerialLine(const QString &line)
@@ -789,6 +918,14 @@ void MainWindow::handleSerialLine(const QString &line)
const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch(); const bool solveStarted = solveStartRegex.match(line.trimmed()).hasMatch();
const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch(); const bool trackingSnapshot = trackingSnapshotRegex.match(line).hasMatch();
if (solveStarted) { 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(); m_observedPrns.clear();
} }
const QSet<QString> linePrns = LocationParser::parseGpsPrns(line); const QSet<QString> linePrns = LocationParser::parseGpsPrns(line);
@@ -1012,7 +1149,10 @@ void MainWindow::buildUi()
background: #23445d; background: #23445d;
border-color: #62b8e8; border-color: #62b8e8;
} }
QPushButton:pressed, QToolButton:pressed { background: #dbeaf6; } QPushButton:pressed, QToolButton:pressed {
color: #102333;
background: #dbeaf6;
}
QCheckBox { spacing: 7px; color: #d5e0e9; } QCheckBox { spacing: 7px; color: #d5e0e9; }
QCheckBox::indicator { width: 17px; height: 17px; } QCheckBox::indicator { width: 17px; height: 17px; }
QCheckBox::indicator:unchecked { QCheckBox::indicator:unchecked {
@@ -1089,7 +1229,7 @@ void MainWindow::buildUi()
m_portCombo = new QComboBox(this); m_portCombo = new QComboBox(this);
m_portCombo->setObjectName(QStringLiteral("portSelector")); m_portCombo->setObjectName(QStringLiteral("portSelector"));
m_portCombo->setMinimumWidth(220); m_portCombo->setMinimumWidth(170);
m_portCombo->setEditable(true); m_portCombo->setEditable(true);
m_portCombo->setToolTip(QStringLiteral( m_portCombo->setToolTip(QStringLiteral(
"USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir.")); "USB UART portu. macOS'ta /dev/cu.usb... tercih edilir; /dev/tty.usb... da desteklenir."));
@@ -1124,6 +1264,11 @@ void MainWindow::buildUi()
m_replayButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); m_replayButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
m_replayButton->setToolTip(QStringLiteral( m_replayButton->setToolTip(QStringLiteral(
"Kayitli UART verisini GPS ve sensor guncelleme hizlariyla yeniden oynat")); "Kayitli UART verisini GPS ve sensor guncelleme hizlariyla yeniden oynat"));
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 = new QCheckBox(QStringLiteral("Kamerayi takip et"), this);
m_autoFollowCheck->setChecked(true); m_autoFollowCheck->setChecked(true);
@@ -1145,6 +1290,7 @@ void MainWindow::buildUi()
toolbarLayout->addWidget(m_connectButton); toolbarLayout->addWidget(m_connectButton);
toolbarLayout->addWidget(m_openLogButton); toolbarLayout->addWidget(m_openLogButton);
toolbarLayout->addWidget(m_replayButton); toolbarLayout->addWidget(m_replayButton);
toolbarLayout->addWidget(m_rinexUpdateButton);
toolbarLayout->addWidget(m_clearButton); toolbarLayout->addWidget(m_clearButton);
toolbarLayout->addWidget(m_autoFollowCheck); toolbarLayout->addWidget(m_autoFollowCheck);
toolbarLayout->addWidget(zoomOutButton); toolbarLayout->addWidget(zoomOutButton);
@@ -1178,7 +1324,7 @@ void MainWindow::buildUi()
viewTabs->addTab(globeTab, QStringLiteral("3B Dunya · Uydular")); viewTabs->addTab(globeTab, QStringLiteral("3B Dunya · Uydular"));
auto *sidePanel = new QWidget(this); auto *sidePanel = new QWidget(this);
sidePanel->setMinimumWidth(390); sidePanel->setMinimumWidth(330);
auto *sideOuterLayout = new QVBoxLayout(sidePanel); auto *sideOuterLayout = new QVBoxLayout(sidePanel);
sideOuterLayout->setContentsMargins(8, 0, 0, 0); sideOuterLayout->setContentsMargins(8, 0, 0, 0);
sideOuterLayout->setSpacing(0); sideOuterLayout->setSpacing(0);
@@ -1381,7 +1527,11 @@ void MainWindow::buildUi()
connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints); connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints);
connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog); connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog);
connect(m_replayButton, &QPushButton::clicked, this, &MainWindow::openUartReplay); connect(m_replayButton, &QPushButton::clicked, this, &MainWindow::openUartReplay);
connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts); connect(m_rinexUpdateButton, &QPushButton::clicked, m_rinex, &RinexNavigation::refresh);
connect(refreshPortsButton, &QToolButton::clicked, this, [this] {
m_portsInitialized = false;
refreshSerialPorts();
});
connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this, connect(m_maxPointsSpin, qOverload<int>(&QSpinBox::valueChanged), this,
&MainWindow::trimToMaxPoints); &MainWindow::trimToMaxPoints);
connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow); connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow);
+7
View File
@@ -59,6 +59,9 @@ private slots:
private: private:
void buildUi(); void buildUi();
void buildMenus();
void loadSettings();
void saveSettings() const;
void updateConnectionControls(); void updateConnectionControls();
void addTelemetryPoint(const ParsedTelemetry &telemetry); void addTelemetryPoint(const ParsedTelemetry &telemetry);
void refreshTable(); void refreshTable();
@@ -89,6 +92,7 @@ private:
QPushButton *m_clearButton = nullptr; QPushButton *m_clearButton = nullptr;
QPushButton *m_openLogButton = nullptr; QPushButton *m_openLogButton = nullptr;
QPushButton *m_replayButton = nullptr; QPushButton *m_replayButton = nullptr;
QPushButton *m_rinexUpdateButton = nullptr;
QPushButton *m_refreshRinexButton = nullptr; QPushButton *m_refreshRinexButton = nullptr;
QCheckBox *m_autoFollowCheck = nullptr; QCheckBox *m_autoFollowCheck = nullptr;
QCheckBox *m_showTelemetryCheck = nullptr; QCheckBox *m_showTelemetryCheck = nullptr;
@@ -113,6 +117,7 @@ private:
QPlainTextEdit *m_log = nullptr; QPlainTextEdit *m_log = nullptr;
QTimer *m_locationTimeoutTimer = nullptr; QTimer *m_locationTimeoutTimer = nullptr;
QTimer *m_uiRefreshTimer = nullptr; QTimer *m_uiRefreshTimer = nullptr;
QTimer *m_portRefreshTimer = nullptr;
QFile *m_uartLogFile = nullptr; QFile *m_uartLogFile = nullptr;
QString m_uartLogPath; QString m_uartLogPath;
@@ -125,4 +130,6 @@ private:
DopValues m_lastDop; DopValues m_lastDop;
ParsedTelemetry m_pendingTelemetry; ParsedTelemetry m_pendingTelemetry;
bool m_telemetryRefreshPending = false; bool m_telemetryRefreshPending = false;
bool m_portsInitialized = false;
QStringList m_lastDetectedPorts;
}; };
+261 -54
View File
@@ -1,9 +1,11 @@
#include "MapWidget.h" #include "MapWidget.h"
#include <QDateTime>
#include <QDir> #include <QDir>
#include <QLabel> #include <QLabel>
#include <QLineF> #include <QLineF>
#include <QMouseEvent> #include <QMouseEvent>
#include <QNativeGestureEvent>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkDiskCache> #include <QNetworkDiskCache>
#include <QNetworkRequest> #include <QNetworkRequest>
@@ -13,6 +15,7 @@
#include <QResizeEvent> #include <QResizeEvent>
#include <QStandardPaths> #include <QStandardPaths>
#include <QStringList> #include <QStringList>
#include <QTimer>
#include <QUrl> #include <QUrl>
#include <QWheelEvent> #include <QWheelEvent>
@@ -25,6 +28,8 @@ constexpr int TileSize = 256;
constexpr int MinZoom = 2; constexpr int MinZoom = 2;
constexpr int MaxZoom = 19; constexpr int MaxZoom = 19;
constexpr int MaxMemoryTileCount = 512; constexpr int MaxMemoryTileCount = 512;
constexpr int InitialTileRetryDelayMs = 250;
constexpr int MaximumTileRetryDelayMs = 8000;
constexpr double Pi = 3.14159265358979323846; constexpr double Pi = 3.14159265358979323846;
constexpr double WebMercatorRadiusMeters = 6378137.0; constexpr double WebMercatorRadiusMeters = 6378137.0;
constexpr double MaxMercatorLatitude = 85.05112878; constexpr double MaxMercatorLatitude = 85.05112878;
@@ -34,6 +39,14 @@ QString tileKey(int zoom, int tileX, int tileY)
return QStringLiteral("%1/%2/%3").arg(zoom).arg(tileX).arg(tileY); return QStringLiteral("%1/%2/%3").arg(zoom).arg(tileX).arg(tileY);
} }
QUrl tileUrl(QString urlTemplate, int zoom, int tileX, int tileY)
{
urlTemplate.replace(QStringLiteral("{z}"), QString::number(zoom));
urlTemplate.replace(QStringLiteral("{x}"), QString::number(tileX));
urlTemplate.replace(QStringLiteral("{y}"), QString::number(tileY));
return QUrl(urlTemplate);
}
double clampLatitude(double latitude) double clampLatitude(double latitude)
{ {
return std::clamp(latitude, -MaxMercatorLatitude, MaxMercatorLatitude); return std::clamp(latitude, -MaxMercatorLatitude, MaxMercatorLatitude);
@@ -52,27 +65,16 @@ double metersPerPixel(double latitude, int zoom)
return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale; return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale;
} }
int preferredCurrentPointIndex(const QVector<TelemetryPoint> &points)
{
constexpr qint64 CorrectedPreferenceWindowMs = 1000;
const int newestIndex = points.size() - 1;
for (int index = points.size() - 1; index >= 0; --index) {
if (points[index].source == LocationSource::ImuCorrected) {
const qint64 ageMs = points[index].receivedAt.msecsTo(
points[newestIndex].receivedAt);
return ageMs <= CorrectedPreferenceWindowMs ? index : newestIndex;
}
}
return newestIndex;
}
} // namespace } // namespace
MapWidget::MapWidget(QWidget *parent) MapWidget::MapWidget(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, m_network(new QNetworkAccessManager(this)) , m_network(new QNetworkAccessManager(this))
, m_tileUrlTemplate(qEnvironmentVariable(
"LOCATION_PLOTTER_TILE_URL_TEMPLATE",
QStringLiteral("https://tile.openstreetmap.org/{z}/{x}/{y}.png")))
{ {
setMinimumSize(640, 420); setMinimumSize(480, 320);
setMouseTracking(true); setMouseTracking(true);
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
@@ -104,9 +106,9 @@ void MapWidget::setPoints(const QVector<TelemetryPoint> &points)
m_points = points; m_points = points;
if (!m_points.isEmpty() && m_autoFollow) { if (!m_points.isEmpty() && m_autoFollow) {
const TelemetryPoint &current = m_points[preferredCurrentPointIndex(m_points)]; const TelemetryPoint &focus = m_points[preferredFocusIndex()];
m_centerLat = current.latitude; m_centerLat = focus.latitude;
m_centerLon = current.longitude; m_centerLon = focus.longitude;
if (wasEmpty) { if (wasEmpty) {
m_zoom = MaxZoom; m_zoom = MaxZoom;
} }
@@ -129,23 +131,55 @@ void MapWidget::fitToPoints()
} }
double minimumLatitude = m_points.first().latitude; double minimumLatitude = m_points.first().latitude;
double maximumLatitude = minimumLatitude; double maximumLatitude = minimumLatitude;
double minimumLongitude = m_points.first().longitude; QVector<double> longitudes;
double maximumLongitude = minimumLongitude; longitudes.reserve(m_points.size());
for (const TelemetryPoint &point : m_points) { for (const TelemetryPoint &point : m_points) {
minimumLatitude = std::min(minimumLatitude, point.latitude); minimumLatitude = std::min(minimumLatitude, point.latitude);
maximumLatitude = std::max(maximumLatitude, point.latitude); maximumLatitude = std::max(maximumLatitude, point.latitude);
minimumLongitude = std::min(minimumLongitude, point.longitude); double normalizedLongitude = std::fmod(point.longitude + 360.0, 360.0);
maximumLongitude = std::max(maximumLongitude, point.longitude); 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_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 availableWidth = std::max(100.0, width() * 0.82);
const double availableHeight = std::max(100.0, height() * 0.82); const double availableHeight = std::max(100.0, height() * 0.82);
for (int zoom = MaxZoom; zoom >= MinZoom; --zoom) { for (int zoom = MaxZoom; zoom >= MinZoom; --zoom) {
const QPointF topLeft = coordinateToWorld(maximumLatitude, minimumLongitude, zoom); const double worldWidth = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const QPointF bottomRight = coordinateToWorld(minimumLatitude, maximumLongitude, zoom); const double longitudeWidth = longitudeSpan / 360.0 * worldWidth;
if (std::fabs(bottomRight.x() - topLeft.x()) <= availableWidth const double top = coordinateToWorld(maximumLatitude, m_centerLon, zoom).y();
&& std::fabs(bottomRight.y() - topLeft.y()) <= availableHeight) { const double bottom = coordinateToWorld(minimumLatitude, m_centerLon, zoom).y();
if (longitudeWidth <= availableWidth
&& std::fabs(bottom - top) <= availableHeight) {
m_zoom = zoom; m_zoom = zoom;
break; break;
} }
@@ -218,9 +252,9 @@ void MapWidget::setAutoFollow(bool enabled)
m_autoFollow = enabled; m_autoFollow = enabled;
if (m_autoFollow && !m_points.isEmpty()) { if (m_autoFollow && !m_points.isEmpty()) {
const TelemetryPoint &current = m_points[preferredCurrentPointIndex(m_points)]; const TelemetryPoint &focus = m_points[preferredFocusIndex()];
m_centerLat = current.latitude; m_centerLat = focus.latitude;
m_centerLon = current.longitude; m_centerLon = focus.longitude;
update(); update();
} }
emit autoFollowChanged(m_autoFollow); emit autoFollowChanged(m_autoFollow);
@@ -279,16 +313,52 @@ void MapWidget::paintEvent(QPaintEvent *)
void MapWidget::wheelEvent(QWheelEvent *event) void MapWidget::wheelEvent(QWheelEvent *event)
{ {
const int delta = event->angleDelta().y(); const QPoint pixelDelta = event->pixelDelta();
if (delta == 0) { const QPoint angleDelta = event->angleDelta();
const double delta = !pixelDelta.isNull()
? pixelDelta.y() / 36.0
: angleDelta.y() / 120.0;
if (qFuzzyIsNull(delta)) {
return; return;
} }
disableAutoFollowFromUserAction(); 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(); 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) void MapWidget::mousePressEvent(QMouseEvent *event)
{ {
if (event->button() == Qt::RightButton) { if (event->button() == Qt::RightButton) {
@@ -405,6 +475,15 @@ void MapWidget::drawTiles(QPainter &painter)
painter.setRenderHint(QPainter::SmoothPixmapTransform, false); painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
struct MissingTile {
int x;
int y;
double distanceFromCenter;
};
QVector<MissingTile> missingTiles;
QSet<QString> visibleTileKeys;
const QPointF viewportCenter = topLeft + QPointF(width() / 2.0, height() / 2.0);
for (int tileY = firstTileY; tileY <= lastTileY; ++tileY) { for (int tileY = firstTileY; tileY <= lastTileY; ++tileY) {
if (tileY < 0 || tileY >= tileCount) { if (tileY < 0 || tileY >= tileCount) {
continue; continue;
@@ -413,6 +492,7 @@ void MapWidget::drawTiles(QPainter &painter)
for (int tileX = firstTileX; tileX <= lastTileX; ++tileX) { for (int tileX = firstTileX; tileX <= lastTileX; ++tileX) {
const int wrappedTileX = positiveModulo(tileX, tileCount); const int wrappedTileX = positiveModulo(tileX, tileCount);
const QString key = tileKey(m_zoom, wrappedTileX, tileY); const QString key = tileKey(m_zoom, wrappedTileX, tileY);
visibleTileKeys.insert(key);
const QRectF target( const QRectF target(
tileX * TileSize - topLeft.x(), tileX * TileSize - topLeft.x(),
tileY * TileSize - topLeft.y(), tileY * TileSize - topLeft.y(),
@@ -421,15 +501,57 @@ void MapWidget::drawTiles(QPainter &painter)
const auto tileIt = m_tileCache.constFind(key); const auto tileIt = m_tileCache.constFind(key);
if (tileIt != m_tileCache.constEnd()) { if (tileIt != m_tileCache.constEnd()) {
painter.drawPixmap(target.toRect(), *tileIt); painter.drawPixmap(target, *tileIt, QRectF(tileIt->rect()));
} else { } else {
if (!drawCachedTileOrParent(painter, target, wrappedTileX, tileY, m_zoom)) {
painter.fillRect(target, QColor(230, 234, 230)); painter.fillRect(target, QColor(230, 234, 230));
painter.setPen(QColor(198, 205, 200)); painter.setPen(QColor(198, 205, 200));
painter.drawRect(target); painter.drawRect(target);
requestTile(wrappedTileX, tileY, m_zoom); }
const QPointF tileCenter((tileX + 0.5) * TileSize,
(tileY + 0.5) * TileSize);
const QPointF offset = tileCenter - viewportCenter;
missingTiles.push_back({wrappedTileX, tileY,
offset.x() * offset.x() + offset.y() * offset.y()});
} }
} }
} }
// A fast pan or auto-follow jump can otherwise leave old requests ahead of
// the tiles that are visible now. Cancel those requests and enqueue the
// centre tiles first so the viewport fills in promptly.
cancelInvisibleTileRequests(visibleTileKeys);
std::sort(missingTiles.begin(), missingTiles.end(),
[](const MissingTile &left, const MissingTile &right) {
return left.distanceFromCenter < right.distanceFromCenter;
});
for (const MissingTile &tile : missingTiles) {
requestTile(tile.x, tile.y, m_zoom);
}
}
bool MapWidget::drawCachedTileOrParent(QPainter &painter, const QRectF &target,
int tileX, int tileY, int zoom) const
{
for (int parentZoom = zoom - 1; parentZoom >= MinZoom; --parentZoom) {
const int scale = 1 << (zoom - parentZoom);
const int parentX = tileX / scale;
const int parentY = tileY / scale;
const auto parentIt = m_tileCache.constFind(tileKey(parentZoom, parentX, parentY));
if (parentIt == m_tileCache.constEnd()) {
continue;
}
const QPixmap &parent = *parentIt;
const double sourceWidth = static_cast<double>(parent.width()) / scale;
const double sourceHeight = static_cast<double>(parent.height()) / scale;
const QRectF source((tileX % scale) * sourceWidth,
(tileY % scale) * sourceHeight,
sourceWidth, sourceHeight);
painter.drawPixmap(target, parent, source);
return true;
}
return false;
} }
void MapWidget::drawTelemetry(QPainter &painter) void MapWidget::drawTelemetry(QPainter &painter)
@@ -472,11 +594,10 @@ void MapWidget::drawTelemetry(QPainter &painter)
drawCepMarker(painter); drawCepMarker(painter);
const bool drawNumbers = m_points.size() <= 150; const bool drawNumbers = m_points.size() <= 150;
const int currentPointIndex = preferredCurrentPointIndex(m_points); const int focusIndex = preferredFocusIndex();
const int firstPoint = m_showRecentPoints ? 0 : currentPointIndex; for (int i = 0; i < m_points.size(); ++i) {
const int endPoint = m_showRecentPoints ? m_points.size() : currentPointIndex + 1; if (!m_showRecentPoints && i != focusIndex) continue;
for (int i = firstPoint; i < endPoint; ++i) { const bool isLast = i == focusIndex;
const bool isLast = i == currentPointIndex;
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude); const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
const double radius = isLast ? 8.0 : 5.0; const double radius = isLast ? 8.0 : 5.0;
@@ -499,7 +620,7 @@ void MapWidget::drawTelemetry(QPainter &painter)
} }
} }
const TelemetryPoint &last = m_points[currentPointIndex]; const TelemetryPoint &last = m_points[focusIndex];
const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude); const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude);
QStringList lines; QStringList lines;
const QString latitudeText = last.latitudeText.isEmpty() const QString latitudeText = last.latitudeText.isEmpty()
@@ -607,9 +728,10 @@ void MapWidget::drawCepMarker(QPainter &painter)
painter.drawLine(QPointF(screen.x(), screen.y() - 11.0), painter.drawLine(QPointF(screen.x(), screen.y() - 11.0),
QPointF(screen.x(), screen.y() + 11.0)); QPointF(screen.x(), screen.y() + 11.0));
const bool overlapsLast = !m_points.isEmpty() const int focusIndex = preferredFocusIndex();
&& QLineF(screen, coordinateToScreen(m_points.last().latitude, const bool overlapsLast = focusIndex >= 0
m_points.last().longitude)).length() < 30.0; && QLineF(screen, coordinateToScreen(m_points[focusIndex].latitude,
m_points[focusIndex].longitude)).length() < 30.0;
if (!overlapsLast) { if (!overlapsLast) {
painter.setFont(QFont(painter.font().family(), 9, QFont::Bold)); painter.setFont(QFont(painter.font().family(), 9, QFont::Bold));
painter.setPen(QColor(20, 83, 78)); painter.setPen(QColor(20, 83, 78));
@@ -643,9 +765,10 @@ void MapWidget::drawAverageOverlay(QPainter &painter)
painter.drawLine(QPointF(screen.x() - 12.0, screen.y()), QPointF(screen.x() + 12.0, screen.y())); painter.drawLine(QPointF(screen.x() - 12.0, screen.y()), QPointF(screen.x() + 12.0, screen.y()));
painter.drawLine(QPointF(screen.x(), screen.y() - 12.0), QPointF(screen.x(), screen.y() + 12.0)); painter.drawLine(QPointF(screen.x(), screen.y() - 12.0), QPointF(screen.x(), screen.y() + 12.0));
const bool overlapsLast = !m_points.isEmpty() const int focusIndex = preferredFocusIndex();
&& QLineF(screen, coordinateToScreen(m_points.last().latitude, const bool overlapsLast = focusIndex >= 0
m_points.last().longitude)).length() < 30.0; && QLineF(screen, coordinateToScreen(m_points[focusIndex].latitude,
m_points[focusIndex].longitude)).length() < 30.0;
if (!overlapsLast) { if (!overlapsLast) {
painter.setFont(QFont(painter.font().family(), 9, QFont::Bold)); painter.setFont(QFont(painter.font().family(), 9, QFont::Bold));
const QRectF labelRect(screen + QPointF(12.0, 8.0), QSizeF(72.0, 20.0)); const QRectF labelRect(screen + QPointF(12.0, 8.0), QSizeF(72.0, 20.0));
@@ -670,20 +793,27 @@ void MapWidget::requestTile(int tileX, int tileY, int zoom)
return; return;
} }
const qint64 now = QDateTime::currentMSecsSinceEpoch();
if (m_tileRetryAfterMs.value(key, 0) > now) {
return;
}
m_pendingTiles.insert(key); m_pendingTiles.insert(key);
const QUrl url(QStringLiteral("https://tile.openstreetmap.org/%1/%2/%3.png") QNetworkRequest request(tileUrl(m_tileUrlTemplate, zoom, tileX, tileY));
.arg(zoom) request.setRawHeader("User-Agent", "RocketLocationPlotter/0.3 (Qt desktop application)");
.arg(tileX) request.setTransferTimeout(15000);
.arg(tileY));
QNetworkRequest request(url);
request.setRawHeader("User-Agent", "RocketLocationPlotter/0.2 (Qt desktop application)");
QNetworkReply *reply = m_network->get(request); QNetworkReply *reply = m_network->get(request);
reply->setProperty("tileKey", key); reply->setProperty("tileKey", key);
m_tileReplies.insert(key, reply);
connect(reply, &QNetworkReply::finished, this, [this, reply]() { connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QString key = reply->property("tileKey").toString(); const QString key = reply->property("tileKey").toString();
m_pendingTiles.remove(key); m_pendingTiles.remove(key);
if (m_tileReplies.value(key) == reply) {
m_tileReplies.remove(key);
}
bool loaded = false;
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QPixmap pixmap; QPixmap pixmap;
if (pixmap.loadFromData(reply->readAll())) { if (pixmap.loadFromData(reply->readAll())) {
@@ -691,14 +821,77 @@ void MapWidget::requestTile(int tileX, int tileY, int zoom)
m_tileCache.erase(m_tileCache.begin()); m_tileCache.erase(m_tileCache.begin());
} }
m_tileCache.insert(key, pixmap); m_tileCache.insert(key, pixmap);
update(); m_tileFailureCounts.remove(key);
m_tileRetryAfterMs.remove(key);
loaded = true;
} }
} }
const bool cancelled = reply->property("tileCancelled").toBool();
if (!loaded && !cancelled) {
const int failures = m_tileFailureCounts.value(key, 0) + 1;
m_tileFailureCounts.insert(key, failures);
const int shift = std::min(failures - 1, 5);
const int retryDelay = std::min(MaximumTileRetryDelayMs,
InitialTileRetryDelayMs * (1 << shift));
m_tileRetryAfterMs.insert(key,
QDateTime::currentMSecsSinceEpoch() + retryDelay);
QTimer::singleShot(retryDelay, this, [this, key]() {
repaintWhenTileRetryIsReady(key);
});
}
if (loaded) {
update();
}
reply->deleteLater(); reply->deleteLater();
}); });
} }
void MapWidget::cancelInvisibleTileRequests(const QSet<QString> &visibleTileKeys)
{
QVector<QNetworkReply *> obsoleteReplies;
for (auto it = m_tileReplies.cbegin(); it != m_tileReplies.cend(); ++it) {
if (!visibleTileKeys.contains(it.key())) {
obsoleteReplies.push_back(it.value());
}
}
for (QNetworkReply *reply : obsoleteReplies) {
if (reply == nullptr || reply->isFinished()) {
continue;
}
reply->setProperty("tileCancelled", true);
reply->abort();
}
for (auto it = m_tileRetryAfterMs.begin(); it != m_tileRetryAfterMs.end();) {
if (visibleTileKeys.contains(it.key())) {
++it;
continue;
}
m_tileFailureCounts.remove(it.key());
it = m_tileRetryAfterMs.erase(it);
}
}
void MapWidget::repaintWhenTileRetryIsReady(const QString &key)
{
if (!m_tileRetryAfterMs.contains(key)) {
return;
}
const qint64 remaining = m_tileRetryAfterMs.value(key, 0)
- QDateTime::currentMSecsSinceEpoch();
if (remaining > 0) {
QTimer::singleShot(static_cast<int>(remaining), this, [this, key]() {
repaintWhenTileRetryIsReady(key);
});
return;
}
update();
}
void MapWidget::setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor) void MapWidget::setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor)
{ {
newZoom = std::clamp(newZoom, MinZoom, MaxZoom); newZoom = std::clamp(newZoom, MinZoom, MaxZoom);
@@ -725,3 +918,17 @@ void MapWidget::disableAutoFollowFromUserAction()
m_autoFollow = false; m_autoFollow = false;
emit autoFollowChanged(false); emit autoFollowChanged(false);
} }
int MapWidget::preferredFocusIndex() const
{
constexpr qint64 CorrectedPreferenceWindowMs = 1000;
const int newestIndex = m_points.size() - 1;
for (int index = m_points.size() - 1; index >= 0; --index) {
if (m_points[index].source == LocationSource::ImuCorrected) {
const qint64 ageMs = m_points[index].receivedAt.msecsTo(
m_points[newestIndex].receivedAt);
return ageMs <= CorrectedPreferenceWindowMs ? index : newestIndex;
}
}
return newestIndex;
}
+12
View File
@@ -11,6 +11,7 @@
#include <optional> #include <optional>
class QNetworkAccessManager; class QNetworkAccessManager;
class QNetworkReply;
class QLabel; class QLabel;
class MapWidget : public QWidget { class MapWidget : public QWidget {
@@ -42,6 +43,7 @@ signals:
void referenceLocationSelected(double latitude, double longitude); void referenceLocationSelected(double latitude, double longitude);
protected: protected:
bool event(QEvent *event) override;
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void wheelEvent(QWheelEvent *event) override; void wheelEvent(QWheelEvent *event) override;
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
@@ -63,15 +65,24 @@ private:
void drawCepOverlay(QPainter &painter); void drawCepOverlay(QPainter &painter);
void drawCepMarker(QPainter &painter); void drawCepMarker(QPainter &painter);
void drawEmptyState(QPainter &painter); void drawEmptyState(QPainter &painter);
bool drawCachedTileOrParent(QPainter &painter, const QRectF &target,
int tileX, int tileY, int zoom) const;
void requestTile(int tileX, int tileY, int zoom); void requestTile(int tileX, int tileY, int zoom);
void cancelInvisibleTileRequests(const QSet<QString> &visibleTileKeys);
void repaintWhenTileRetryIsReady(const QString &key);
void setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor); void setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor);
void disableAutoFollowFromUserAction(); void disableAutoFollowFromUserAction();
int preferredFocusIndex() const;
QVector<TelemetryPoint> m_points; QVector<TelemetryPoint> m_points;
QNetworkAccessManager *m_network = nullptr; QNetworkAccessManager *m_network = nullptr;
QLabel *m_attributionLabel = nullptr; QLabel *m_attributionLabel = nullptr;
QHash<QString, QPixmap> m_tileCache; QHash<QString, QPixmap> m_tileCache;
QSet<QString> m_pendingTiles; QSet<QString> m_pendingTiles;
QHash<QString, QNetworkReply *> m_tileReplies;
QHash<QString, int> m_tileFailureCounts;
QHash<QString, qint64> m_tileRetryAfterMs;
QString m_tileUrlTemplate;
double m_centerLat = 39.0; double m_centerLat = 39.0;
double m_centerLon = 35.0; double m_centerLon = 35.0;
@@ -90,4 +101,5 @@ private:
std::optional<double> m_cepRadiusMeters; std::optional<double> m_cepRadiusMeters;
bool m_dragging = false; bool m_dragging = false;
QPoint m_lastMousePos; QPoint m_lastMousePos;
double m_wheelZoomAccumulator = 0.0;
}; };
+4 -1
View File
@@ -6,6 +6,7 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTimeZone>
#include <QUrl> #include <QUrl>
#include <zlib.h> #include <zlib.h>
@@ -148,7 +149,9 @@ void RinexNavigation::handleDownload(const QByteArray &compressed, const QDate &
return; return;
} }
setLoading(false); setLoading(false);
if (!loadCache()) {
emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true); emit statusChanged(QStringLiteral("RINEX okunamadi: %1").arg(error), true);
}
return; return;
} }
@@ -180,7 +183,7 @@ bool RinexNavigation::loadCache()
emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2") emit statusChanged(QStringLiteral("%1 GPS uydusu · onbellekteki RINEX · %2")
.arg(positions.size()) .arg(positions.size())
.arg(now.toLocalTime().toString( .arg(now.toLocalTime().toString(
QStringLiteral("HH:mm:ss t"))), true); QStringLiteral("HH:mm:ss t"))), false);
return true; return true;
} }
+50
View File
@@ -3,6 +3,7 @@
#include "../src/MapWidget.h" #include "../src/MapWidget.h"
#include <QApplication> #include <QApplication>
#include <QAction>
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
@@ -21,6 +22,7 @@
#include <QTemporaryDir> #include <QTemporaryDir>
#include <cassert> #include <cassert>
#include <cmath>
namespace { namespace {
@@ -88,6 +90,10 @@ int main(int argc, char **argv)
auto *map = window.findChild<MapWidget *>(); auto *map = window.findChild<MapWidget *>();
auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs")); auto *mainViewTabs = window.findChild<QTabWidget *>(QStringLiteral("mainViewTabs"));
auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter")); auto *sideSplitter = window.findChild<QSplitter *>(QStringLiteral("sideSplitter"));
auto *rinexUpdateButton = window.findChild<QPushButton *>(QStringLiteral("rinexUpdateButton"));
auto *openLogAction = window.findChild<QAction *>(QStringLiteral("openLogAction"));
auto *toggleSerialAction = window.findChild<QAction *>(QStringLiteral("toggleSerialAction"));
auto *rinexUpdateAction = window.findChild<QAction *>(QStringLiteral("rinexUpdateAction"));
assert(table != nullptr); assert(table != nullptr);
assert(referenceLatInput != nullptr); assert(referenceLatInput != nullptr);
assert(referenceLonInput != nullptr); assert(referenceLonInput != nullptr);
@@ -118,6 +124,32 @@ int main(int argc, char **argv)
assert(mainViewTabs->count() == 2); assert(mainViewTabs->count() == 2);
assert(sideSplitter != nullptr); assert(sideSplitter != nullptr);
assert(sideSplitter->orientation() == Qt::Vertical); assert(sideSplitter->orientation() == Qt::Vertical);
assert(rinexUpdateButton != nullptr);
assert(!rinexUpdateButton->toolTip().isEmpty());
assert(openLogAction != nullptr && !openLogAction->shortcut().isEmpty());
assert(toggleSerialAction != nullptr && !toggleSerialAction->shortcut().isEmpty());
assert(rinexUpdateAction != nullptr && !rinexUpdateAction->shortcut().isEmpty());
QVector<TelemetryPoint> followPoints;
TelemetryPoint gpsPoint;
gpsPoint.source = LocationSource::Gps;
gpsPoint.latitude = 40.0;
gpsPoint.longitude = 29.0;
followPoints.push_back(gpsPoint);
TelemetryPoint correctedPoint = gpsPoint;
correctedPoint.source = LocationSource::ImuCorrected;
correctedPoint.latitude = 40.25;
correctedPoint.longitude = 29.5;
followPoints.push_back(correctedPoint);
TelemetryPoint trajectoryPoint = gpsPoint;
trajectoryPoint.source = LocationSource::Trajectory;
trajectoryPoint.latitude = 41.0;
trajectoryPoint.longitude = 30.0;
followPoints.push_back(trajectoryPoint);
map->setPoints(followPoints);
assert(std::abs(map->centerCoordinate().x() - 40.25) < 0.000001);
assert(std::abs(map->centerCoordinate().y() - 29.5) < 0.000001);
map->clearPoints();
sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prn_indices=[2,7]")); sendLine(window, QStringLiteral("[TRK] tracked satellites=2 prn_indices=[2,7]"));
assert(map != nullptr); assert(map != nullptr);
@@ -185,6 +217,24 @@ int main(int argc, char **argv)
assert(table->model()->data(table->model()->index(0, 1)).toString() assert(table->model()->data(table->model()->index(0, 1)).toString()
== QStringLiteral("Trajectory (truth)")); == 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)); assert(QMetaObject::invokeMethod(&window, "clearPoints", Qt::DirectConnection));
sendLine(window, QStringLiteral( sendLine(window, QStringLiteral(
"lat=40.78925034214206846173 long=29.46277530841727809729")); "lat=40.78925034214206846173 long=29.46277530841727809729"));
+73
View File
@@ -0,0 +1,73 @@
#include "../src/MapWidget.h"
#include <QApplication>
#include <QElapsedTimer>
#include <QHostAddress>
#include <QSet>
#include <QTcpServer>
#include <QTcpSocket>
#include <QThread>
#include <cassert>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QTcpServer tileServer;
assert(tileServer.listen(QHostAddress::LocalHost, 0));
int requestCount = 0;
QSet<QByteArray> requestedPaths;
QObject::connect(&tileServer, &QTcpServer::newConnection, [&]() {
while (tileServer.hasPendingConnections()) {
QTcpSocket *socket = tileServer.nextPendingConnection();
const auto respond = [&, socket]() {
if (!socket->canReadLine()) {
return;
}
const QByteArray requestLine = socket->readLine().trimmed();
const QList<QByteArray> fields = requestLine.split(' ');
if (fields.size() >= 2) {
++requestCount;
requestedPaths.insert(fields[1]);
}
socket->readAll();
socket->write("HTTP/1.1 503 Service Unavailable\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n\r\n");
socket->disconnectFromHost();
};
QObject::connect(socket, &QTcpSocket::readyRead, socket, respond);
QObject::connect(socket, &QTcpSocket::disconnected,
socket, &QObject::deleteLater);
respond();
}
});
const QByteArray tileUrl = QByteArray("http://127.0.0.1:")
+ QByteArray::number(tileServer.serverPort()) + "/{z}/{x}/{y}.png";
qputenv("LOCATION_PLOTTER_TILE_URL_TEMPLATE", tileUrl);
{
MapWidget map;
map.resize(640, 480);
map.show();
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < 1200) {
app.processEvents(QEventLoop::AllEvents, 50);
QThread::msleep(5);
}
// Every URL fails. Seeing any URL more than once proves that a failed
// tile schedules a new paint/request instead of remaining a blank
// placeholder forever.
assert(!requestedPaths.isEmpty());
assert(requestCount > requestedPaths.size());
}
qunsetenv("LOCATION_PLOTTER_TILE_URL_TEMPLATE");
return 0;
}