73227452c3
# Conflicts: # src/GlobeWidget.cpp # src/GlobeWidget.h # src/MainWindow.h # src/MapWidget.cpp # src/MapWidget.h # src/RinexNavigation.cpp
798 lines
27 KiB
C++
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;
|
|
}
|