#include "MapWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(TileSize) * static_cast(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("") + QString::fromUtf8("\xC2\xA9 OpenStreetMap contributors") + QStringLiteral("")); 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 &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 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(TileSize) * static_cast(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 &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(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(TileSize) * static_cast(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(TileSize) * static_cast(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(TileSize) * static_cast(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(std::floor(topLeft.x() / TileSize)); const int lastTileX = static_cast(std::floor((topLeft.x() + width()) / TileSize)); const int firstTileY = static_cast(std::floor(topLeft.y() / TileSize)); const int lastTileY = static_cast(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; }