Files
location_plotter/src/MapWidget.cpp
T
onur 73227452c3
Ubuntu build / build-and-test (ubuntu-22.04) (push) Has been cancelled
Ubuntu build / build-and-test (ubuntu-24.04) (push) Has been cancelled
Merge GUI updates into macOS branch
# 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

798 lines
27 KiB
C++

#include "MapWidget.h"
#include <QDir>
#include <QLabel>
#include <QLineF>
#include <QMouseEvent>
#include <QNativeGestureEvent>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QPainter>
#include <QPainterPath>
#include <QResizeEvent>
#include <QStandardPaths>
#include <QStringList>
#include <QUrl>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
namespace {
constexpr int TileSize = 256;
constexpr int MinZoom = 2;
constexpr int MaxZoom = 19;
constexpr int MaxMemoryTileCount = 512;
constexpr double Pi = 3.14159265358979323846;
constexpr double WebMercatorRadiusMeters = 6378137.0;
constexpr double MaxMercatorLatitude = 85.05112878;
QString tileKey(int zoom, int tileX, int tileY)
{
return QStringLiteral("%1/%2/%3").arg(zoom).arg(tileX).arg(tileY);
}
double clampLatitude(double latitude)
{
return std::clamp(latitude, -MaxMercatorLatitude, MaxMercatorLatitude);
}
int positiveModulo(int value, int modulo)
{
const int result = value % modulo;
return result < 0 ? result + modulo : result;
}
double metersPerPixel(double latitude, int zoom)
{
const double scale = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const double latitudeScale = std::cos(latitude * Pi / 180.0);
return latitudeScale * 2.0 * Pi * WebMercatorRadiusMeters / scale;
}
} // namespace
MapWidget::MapWidget(QWidget *parent)
: QWidget(parent)
, m_network(new QNetworkAccessManager(this))
{
setMinimumSize(480, 320);
setMouseTracking(true);
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
if (!cacheRoot.isEmpty()) {
auto *diskCache = new QNetworkDiskCache(m_network);
const QString cacheDirectory = cacheRoot + QStringLiteral("/map-tiles");
QDir().mkpath(cacheDirectory);
diskCache->setCacheDirectory(cacheDirectory);
diskCache->setMaximumCacheSize(100 * 1024 * 1024);
m_network->setCache(diskCache);
}
m_attributionLabel = new QLabel(this);
m_attributionLabel->setText(
QStringLiteral("<a style=\"color:#174b7a;text-decoration:none\" "
"href=\"https://www.openstreetmap.org/copyright\">")
+ QString::fromUtf8("\xC2\xA9 OpenStreetMap contributors")
+ QStringLiteral("</a>"));
m_attributionLabel->setOpenExternalLinks(true);
m_attributionLabel->setStyleSheet(QStringLiteral(
"QLabel { background: rgba(255, 255, 255, 220); color: #26313d; "
"border: 1px solid rgba(120, 130, 140, 90); padding: 3px 6px; }"));
m_attributionLabel->adjustSize();
}
void MapWidget::setPoints(const QVector<TelemetryPoint> &points)
{
const bool wasEmpty = m_points.isEmpty();
m_points = points;
if (!m_points.isEmpty() && m_autoFollow) {
const TelemetryPoint &focus = m_points[preferredFocusIndex()];
m_centerLat = focus.latitude;
m_centerLon = focus.longitude;
if (wasEmpty) {
m_zoom = MaxZoom;
}
}
update();
}
void MapWidget::clearPoints()
{
m_points.clear();
m_hasAverageMarker = false;
update();
}
void MapWidget::fitToPoints()
{
if (m_points.isEmpty()) {
return;
}
double minimumLatitude = m_points.first().latitude;
double maximumLatitude = minimumLatitude;
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);
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 = 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 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;
}
}
if (m_autoFollow) {
m_autoFollow = false;
emit autoFollowChanged(false);
}
update();
}
void MapWidget::setAverageMarker(double latitude, double longitude, double radiusMeters)
{
m_hasAverageMarker = true;
m_averageLat = latitude;
m_averageLon = longitude;
m_averageRadiusMeters = std::max(0.0, radiusMeters);
update();
}
void MapWidget::clearAverageMarker()
{
if (!m_hasAverageMarker) {
return;
}
m_hasAverageMarker = false;
update();
}
void MapWidget::setCepMarker(double latitude, double longitude,
const std::optional<double> &radiusMeters)
{
m_hasCepMarker = true;
m_cepLatitude = latitude;
m_cepLongitude = longitude;
if (m_points.isEmpty()) {
m_centerLat = latitude;
m_centerLon = longitude;
m_zoom = MaxZoom;
}
if (radiusMeters.has_value()) {
m_cepRadiusMeters = std::max(0.0, *radiusMeters);
} else {
m_cepRadiusMeters.reset();
}
update();
}
void MapWidget::clearCepMarker()
{
if (!m_hasCepMarker) {
return;
}
m_hasCepMarker = false;
m_cepRadiusMeters.reset();
update();
}
QPointF MapWidget::centerCoordinate() const
{
return QPointF(m_centerLat, m_centerLon);
}
void MapWidget::setAutoFollow(bool enabled)
{
if (m_autoFollow == enabled) {
return;
}
m_autoFollow = enabled;
if (m_autoFollow && !m_points.isEmpty()) {
const TelemetryPoint &focus = m_points[preferredFocusIndex()];
m_centerLat = focus.latitude;
m_centerLon = focus.longitude;
update();
}
emit autoFollowChanged(m_autoFollow);
}
void MapWidget::setAverageOverlayEnabled(bool enabled)
{
if (m_showAverageOverlay == enabled) {
return;
}
m_showAverageOverlay = enabled;
update();
}
void MapWidget::setTelemetryOverlayEnabled(bool enabled)
{
if (m_showTelemetryOverlay == enabled) {
return;
}
m_showTelemetryOverlay = enabled;
update();
}
void MapWidget::setRecentPointsEnabled(bool enabled)
{
if (m_showRecentPoints == enabled) {
return;
}
m_showRecentPoints = enabled;
update();
}
void MapWidget::zoomIn()
{
setZoomKeepingPoint(m_zoom + 1, QPointF(width() / 2.0, height() / 2.0));
}
void MapWidget::zoomOut()
{
setZoomKeepingPoint(m_zoom - 1, QPointF(width() / 2.0, height() / 2.0));
}
void MapWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(rect(), QColor(219, 225, 220));
drawTiles(painter);
drawTelemetry(painter);
drawLegend(painter);
if (m_points.isEmpty()) {
drawEmptyState(painter);
}
}
void MapWidget::wheelEvent(QWheelEvent *event)
{
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();
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) {
const QPointF coordinate = screenToCoordinate(event->position());
double longitude = std::fmod(coordinate.y() + 180.0, 360.0);
if (longitude < 0.0) {
longitude += 360.0;
}
longitude -= 180.0;
emit referenceLocationSelected(coordinate.x(), longitude);
event->accept();
return;
}
if (event->button() == Qt::LeftButton) {
disableAutoFollowFromUserAction();
m_dragging = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
event->accept();
}
}
void MapWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!m_dragging) {
return;
}
const QPoint delta = event->pos() - m_lastMousePos;
m_lastMousePos = event->pos();
const QPointF centerWorld = coordinateToWorld(m_centerLat, m_centerLon, m_zoom);
const QPointF newCenterWorld = centerWorld - QPointF(delta);
const QPointF coordinate = worldToCoordinate(newCenterWorld, m_zoom);
m_centerLat = coordinate.x();
m_centerLon = coordinate.y();
update();
event->accept();
}
void MapWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && m_dragging) {
m_dragging = false;
unsetCursor();
event->accept();
}
}
void MapWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (m_attributionLabel != nullptr) {
m_attributionLabel->adjustSize();
m_attributionLabel->move(width() - m_attributionLabel->width() - 8,
height() - m_attributionLabel->height() - 8);
m_attributionLabel->raise();
}
}
QPointF MapWidget::coordinateToWorld(double latitude, double longitude, int zoom) const
{
latitude = clampLatitude(latitude);
const double sinLat = std::sin(latitude * Pi / 180.0);
const double scale = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const double x = (longitude + 180.0) / 360.0 * scale;
const double y = (0.5 - std::log((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Pi)) * scale;
return QPointF(x, y);
}
QPointF MapWidget::worldToCoordinate(const QPointF &world, int zoom) const
{
const double scale = static_cast<double>(TileSize) * static_cast<double>(1 << zoom);
const double longitude = world.x() / scale * 360.0 - 180.0;
const double n = Pi - 2.0 * Pi * world.y() / scale;
const double latitude = 180.0 / Pi * std::atan(0.5 * (std::exp(n) - std::exp(-n)));
return QPointF(clampLatitude(latitude), longitude);
}
QPointF MapWidget::coordinateToScreen(double latitude, double longitude) const
{
const QPointF world = coordinateToWorld(latitude, longitude, m_zoom);
const QPointF centerWorld = coordinateToWorld(m_centerLat, m_centerLon, m_zoom);
const double worldWidth = static_cast<double>(TileSize) * static_cast<double>(1 << m_zoom);
double deltaX = world.x() - centerWorld.x();
if (deltaX > worldWidth / 2.0) {
deltaX -= worldWidth;
} else if (deltaX < -worldWidth / 2.0) {
deltaX += worldWidth;
}
return QPointF(width() / 2.0 + deltaX, height() / 2.0 + world.y() - centerWorld.y());
}
QPointF MapWidget::screenToCoordinate(const QPointF &screen) const
{
return worldToCoordinate(topLeftWorld() + screen, m_zoom);
}
QPointF MapWidget::topLeftWorld() const
{
return coordinateToWorld(m_centerLat, m_centerLon, m_zoom)
- QPointF(width() / 2.0, height() / 2.0);
}
void MapWidget::drawTiles(QPainter &painter)
{
const int tileCount = 1 << m_zoom;
const QPointF topLeft = topLeftWorld();
const int firstTileX = static_cast<int>(std::floor(topLeft.x() / TileSize));
const int lastTileX = static_cast<int>(std::floor((topLeft.x() + width()) / TileSize));
const int firstTileY = static_cast<int>(std::floor(topLeft.y() / TileSize));
const int lastTileY = static_cast<int>(std::floor((topLeft.y() + height()) / TileSize));
painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
for (int tileY = firstTileY; tileY <= lastTileY; ++tileY) {
if (tileY < 0 || tileY >= tileCount) {
continue;
}
for (int tileX = firstTileX; tileX <= lastTileX; ++tileX) {
const int wrappedTileX = positiveModulo(tileX, tileCount);
const QString key = tileKey(m_zoom, wrappedTileX, tileY);
const QRectF target(
tileX * TileSize - topLeft.x(),
tileY * TileSize - topLeft.y(),
TileSize,
TileSize);
const auto tileIt = m_tileCache.constFind(key);
if (tileIt != m_tileCache.constEnd()) {
painter.drawPixmap(target.toRect(), *tileIt);
} else {
painter.fillRect(target, QColor(230, 234, 230));
painter.setPen(QColor(198, 205, 200));
painter.drawRect(target);
requestTile(wrappedTileX, tileY, m_zoom);
}
}
}
}
void MapWidget::drawTelemetry(QPainter &painter)
{
if (m_points.isEmpty() || !m_showTelemetryOverlay) {
return;
}
painter.setRenderHint(QPainter::Antialiasing, true);
drawCepOverlay(painter);
const auto drawSourcePath = [this, &painter](LocationSource source, const QColor &color) {
QPainterPath path;
bool havePoint = false;
int pointCount = 0;
for (const TelemetryPoint &point : m_points) {
if (point.source != source) {
continue;
}
const QPointF screen = coordinateToScreen(point.latitude, point.longitude);
havePoint ? path.lineTo(screen) : path.moveTo(screen);
havePoint = true;
++pointCount;
}
if (m_showRecentPoints && pointCount > 1) {
QPen pathPen(color, 3.0);
pathPen.setCapStyle(Qt::RoundCap);
pathPen.setJoinStyle(Qt::RoundJoin);
painter.setPen(pathPen);
painter.drawPath(path);
}
};
drawSourcePath(LocationSource::Trajectory, QColor(22, 163, 74));
drawSourcePath(LocationSource::Generic, QColor(210, 105, 30));
drawSourcePath(LocationSource::Gps, QColor(37, 99, 235));
drawSourcePath(LocationSource::ImuCorrected, QColor(220, 38, 38));
drawAverageOverlay(painter);
drawCepMarker(painter);
const bool drawNumbers = m_points.size() <= 150;
const int focusIndex = preferredFocusIndex();
for (int i = 0; i < m_points.size(); ++i) {
if (!m_showRecentPoints && i != focusIndex) continue;
const bool isLast = i == focusIndex;
const QPointF screen = coordinateToScreen(m_points[i].latitude, m_points[i].longitude);
const double radius = isLast ? 8.0 : 5.0;
const QColor pointColor = m_points[i].source == LocationSource::ImuCorrected
? QColor(220, 38, 38)
: (m_points[i].source == LocationSource::Gps
? QColor(37, 99, 235)
: (m_points[i].source == LocationSource::Trajectory
? QColor(22, 163, 74)
: (isLast ? QColor(210, 105, 30) : QColor(234, 88, 12))));
painter.setPen(QPen(Qt::white, isLast ? 3.0 : 2.0));
painter.setBrush(pointColor);
painter.drawEllipse(screen, radius, radius);
if (drawNumbers) {
painter.setPen(Qt::white);
painter.setFont(QFont(painter.font().family(), 8, QFont::Bold));
painter.drawText(QRectF(screen.x() - 13, screen.y() - 9, 26, 18), Qt::AlignCenter,
QString::number(i + 1));
}
}
const TelemetryPoint &last = m_points[focusIndex];
const QPointF lastScreen = coordinateToScreen(last.latitude, last.longitude);
QStringList lines;
const QString latitudeText = last.latitudeText.isEmpty()
? QString::number(last.latitude, 'f', 15) : last.latitudeText;
const QString longitudeText = last.longitudeText.isEmpty()
? QString::number(last.longitude, 'f', 15) : last.longitudeText;
lines << QStringLiteral("Lat: %1").arg(latitudeText);
lines << QStringLiteral("Long: %1").arg(longitudeText);
lines << QStringLiteral("Kaynak: %1").arg(locationSourceText(last.source));
if (last.hheight.has_value()) {
lines << QStringLiteral("HHeight: %1").arg(*last.hheight, 0, 'f', 2);
}
if (last.satellites.has_value()) {
lines << QStringLiteral("Aktif uydu: %1").arg(*last.satellites);
}
if (m_cepRadiusMeters.has_value()) {
lines << QStringLiteral("CEP50: %1 m").arg(*m_cepRadiusMeters, 0, 'f', 2);
}
painter.setFont(QFont(painter.font().family(), 10));
const QString label = lines.join('\n');
const QRect textRect = painter.fontMetrics().boundingRect(QRect(0, 0, 300, 150),
Qt::TextWordWrap, label);
QRectF bubble(lastScreen + QPointF(14, -textRect.height() - 12),
QSizeF(textRect.width() + 18, textRect.height() + 14));
if (bubble.right() > width() - 8) {
bubble.moveRight(width() - 8);
}
if (bubble.top() < 8) {
bubble.moveTop(8);
}
painter.setPen(QPen(QColor(50, 58, 65, 180), 1.0));
painter.setBrush(QColor(255, 255, 255, 232));
painter.drawRoundedRect(bubble, 6, 6);
painter.setPen(QColor(25, 32, 38));
painter.drawText(bubble.adjusted(9, 7, -9, -7), Qt::AlignLeft | Qt::AlignVCenter, label);
}
void MapWidget::drawLegend(QPainter &painter)
{
if (m_points.isEmpty() || !m_showTelemetryOverlay) {
return;
}
const struct {
const char *text;
QColor color;
} entries[] = {
{"Trajectory (truth)", QColor(22, 163, 74)},
{"GPS (4 satellite)", QColor(37, 99, 235)},
{"IMU + ESKF corrected", QColor(220, 38, 38)}
};
const QRectF box(12.0, 12.0, 190.0, 76.0);
painter.setPen(QPen(QColor(50, 58, 65, 180), 1.0));
painter.setBrush(QColor(255, 255, 255, 232));
painter.drawRoundedRect(box, 6.0, 6.0);
painter.setFont(QFont(painter.font().family(), 9, QFont::DemiBold));
for (int index = 0; index < 3; ++index) {
const double y = 27.0 + index * 22.0;
painter.setPen(QPen(entries[index].color, 3.0));
painter.drawLine(QPointF(23.0, y), QPointF(43.0, y));
painter.setBrush(entries[index].color);
painter.drawEllipse(QPointF(33.0, y), 4.0, 4.0);
painter.setPen(QColor(25, 32, 38));
painter.drawText(QRectF(50.0, y - 9.0, 142.0, 18.0),
Qt::AlignLeft | Qt::AlignVCenter,
QString::fromLatin1(entries[index].text));
}
}
void MapWidget::drawCepOverlay(QPainter &painter)
{
if (!m_hasCepMarker || !m_cepRadiusMeters.has_value() || *m_cepRadiusMeters <= 0.0) {
return;
}
const QPointF screen = coordinateToScreen(m_cepLatitude, m_cepLongitude);
const double resolution = metersPerPixel(m_cepLatitude, m_zoom);
if (!std::isfinite(resolution) || resolution <= 0.0) {
return;
}
const double radiusPixels = std::min(*m_cepRadiusMeters / resolution, 2500.0);
if (radiusPixels < 1.0) {
return;
}
painter.setPen(QPen(QColor(8, 111, 104, 210), 2.0));
painter.setBrush(QColor(13, 148, 136, 42));
painter.drawEllipse(screen, radiusPixels, radiusPixels);
}
void MapWidget::drawCepMarker(QPainter &painter)
{
if (!m_hasCepMarker) {
return;
}
const QPointF screen = coordinateToScreen(m_cepLatitude, m_cepLongitude);
painter.setPen(QPen(QColor(8, 111, 104), 2.5));
painter.setBrush(QColor(255, 255, 255, 230));
painter.drawEllipse(screen, 7.0, 7.0);
painter.drawLine(QPointF(screen.x() - 11.0, screen.y()),
QPointF(screen.x() + 11.0, screen.y()));
painter.drawLine(QPointF(screen.x(), screen.y() - 11.0),
QPointF(screen.x(), screen.y() + 11.0));
const int focusIndex = preferredFocusIndex();
const bool overlapsLast = focusIndex >= 0
&& QLineF(screen, coordinateToScreen(m_points[focusIndex].latitude,
m_points[focusIndex].longitude)).length() < 30.0;
if (!overlapsLast) {
painter.setFont(QFont(painter.font().family(), 9, QFont::Bold));
painter.setPen(QColor(20, 83, 78));
painter.drawText(QRectF(screen + QPointF(12.0, -10.0), QSizeF(110.0, 20.0)),
Qt::AlignLeft | Qt::AlignVCenter, QStringLiteral("Bilinen konum"));
}
}
void MapWidget::drawAverageOverlay(QPainter &painter)
{
if (!m_showAverageOverlay || !m_hasAverageMarker) {
return;
}
const QPointF screen = coordinateToScreen(m_averageLat, m_averageLon);
const double resolution = metersPerPixel(m_averageLat, m_zoom);
if (std::isfinite(resolution) && resolution > 0.0 && m_averageRadiusMeters > 0.0) {
const double radiusPixels = std::min(m_averageRadiusMeters / resolution, 2500.0);
if (radiusPixels >= 2.0) {
painter.setPen(QPen(QColor(124, 58, 237, 150), 2.0, Qt::DashLine));
painter.setBrush(QColor(124, 58, 237, 30));
painter.drawEllipse(screen, radiusPixels, radiusPixels);
}
}
painter.setPen(QPen(Qt::white, 3.0));
painter.setBrush(QColor(124, 58, 237));
painter.drawEllipse(screen, 9.0, 9.0);
painter.setPen(QPen(QColor(25, 32, 38), 2.0));
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));
const int focusIndex = preferredFocusIndex();
const bool overlapsLast = focusIndex >= 0
&& QLineF(screen, coordinateToScreen(m_points[focusIndex].latitude,
m_points[focusIndex].longitude)).length() < 30.0;
if (!overlapsLast) {
painter.setFont(QFont(painter.font().family(), 9, QFont::Bold));
const QRectF labelRect(screen + QPointF(12.0, 8.0), QSizeF(72.0, 20.0));
painter.setPen(QColor(25, 32, 38));
painter.drawText(labelRect, Qt::AlignLeft | Qt::AlignVCenter, QStringLiteral("Ortalama"));
}
}
void MapWidget::drawEmptyState(QPainter &painter)
{
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(QColor(83, 96, 110));
painter.setFont(QFont(painter.font().family(), 13));
painter.drawText(rect(), Qt::AlignCenter,
QStringLiteral("Seri porttan lat / long / hheight bekleniyor"));
}
void MapWidget::requestTile(int tileX, int tileY, int zoom)
{
const QString key = tileKey(zoom, tileX, tileY);
if (m_tileCache.contains(key) || m_pendingTiles.contains(key)) {
return;
}
m_pendingTiles.insert(key);
const QUrl url(QStringLiteral("https://tile.openstreetmap.org/%1/%2/%3.png")
.arg(zoom)
.arg(tileX)
.arg(tileY));
QNetworkRequest request(url);
request.setRawHeader("User-Agent", "RocketLocationPlotter/0.2 (Qt desktop application)");
QNetworkReply *reply = m_network->get(request);
reply->setProperty("tileKey", key);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
const QString key = reply->property("tileKey").toString();
m_pendingTiles.remove(key);
if (reply->error() == QNetworkReply::NoError) {
QPixmap pixmap;
if (pixmap.loadFromData(reply->readAll())) {
if (!m_tileCache.contains(key) && m_tileCache.size() >= MaxMemoryTileCount) {
m_tileCache.erase(m_tileCache.begin());
}
m_tileCache.insert(key, pixmap);
update();
}
}
reply->deleteLater();
});
}
void MapWidget::setZoomKeepingPoint(int newZoom, const QPointF &screenAnchor)
{
newZoom = std::clamp(newZoom, MinZoom, MaxZoom);
if (newZoom == m_zoom) {
return;
}
const QPointF coordinateAtAnchor = screenToCoordinate(screenAnchor);
m_zoom = newZoom;
const QPointF anchorWorld = coordinateToWorld(coordinateAtAnchor.x(), coordinateAtAnchor.y(), m_zoom);
const QPointF newCenterWorld = anchorWorld - (screenAnchor - QPointF(width() / 2.0, height() / 2.0));
const QPointF newCenter = worldToCoordinate(newCenterWorld, m_zoom);
m_centerLat = newCenter.x();
m_centerLon = newCenter.y();
update();
}
void MapWidget::disableAutoFollowFromUserAction()
{
if (!m_autoFollow) {
return;
}
m_autoFollow = 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;
}