C++(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例2

1、概述

  1. 支持多线程加载显示本地离线瓦片地图(墨卡托投影);
  2. 瓦片切片规则以左上角为原点(谷歌、高德、ArcGis等),不支持百度瓦片规则;
  3. 支持显示瓦片网格、编号信息。
  4. 支持鼠标滚轮缩放切换地图层级。
  5. 支持鼠标拖拽。
  6. 采用z/x/y层级瓦片存储格式。
  7. 在单文件中实现所有主要功能,简单便于理解。
  8. 以北纬85.05,西经-180为坐标原点【绝对像素坐标】。

开发环境说明

  • 系统:Windows11、Ubuntu20.04
  • Qt版本:Qt 5.14.2
  • 编译器:MSVC2017-64、GCC/G++64

2、实现效果

使用瓦片地图工具下载z/x/y存储格式的瓦片地图进行显示。

 

3、主要代码

  • bingformula.h

#ifndef BINGFORMULA_H
#define BINGFORMULA_H
#include <QPoint>
#include <QtGlobal>

namespace Bing {
qreal clip(qreal n, qreal min, qreal max);
qreal clipLon(qreal lon);   // 裁剪经度范围
qreal clipLat(qreal lat);   // 裁剪纬度范围

uint mapSize(int level);                        // 根据地图级别计算世界地图总宽高(以像素为单位)
qreal groundResolution(qreal lat, int level);   // 计算地面分辨率
qreal mapScale(qreal lat, int level, int screenDpi);   // 计算比例尺

QPoint latLongToPixelXY(qreal lon, qreal lat, int level);               // 经纬度转像素 XY坐标
void pixelXYToLatLong(QPoint pos, int level, qreal& lon, qreal& lat);   // 像素坐标转WGS-84墨卡托坐标

QPoint pixelXYToTileXY(QPoint pos);    // 像素坐标转瓦片编号
QPoint tileXYToPixelXY(QPoint tile);   // 瓦片编号转像素坐标

QPoint latLongToTileXY(qreal lon, qreal lat, int level);   // 经纬度转瓦片编号
QPointF tileXYToLatLong(QPoint tile, int level);           // 瓦片编号转经纬度

QString tileXYToQuadKey(QPoint tile, int level);                             // 瓦片编号转QuadKey
void quadKeyToTileXY(QString quadKey, int& tileX, int& tileY, int& level);   // QuadKey转瓦片编号、级别
}   // namespace Bing
#endif   // BINGFORMULA_H

bingformula.cpp

/********************************************************************
 * 文件名: bingformula.cpp
 * 时间:   2024-04-05 21:36:16
 * 开发者:  mhf
 * 邮箱:   1603291350@qq.com
 * 说明:   适用于Bing瓦片地图的算法
 * ******************************************************************/
#include "bingformula.h"
#include <qstring.h>
#include <QtMath>

static const qreal g_EarthRadius = 6'378'137;   // 赤道半径

/**
 * @brief      限定最小值,最大值范围
 * @param n    需要限定的值
 * @param min
 * @param max
 * @return
 */
qreal Bing::clip(qreal n, qreal min, qreal max)
{
    n = qMax(n, min);
    n = qMin(n, max);
    return n;
}

/**
 * @brief      限定经度范围值,防止超限,经度范围[-180, 180]
 * @param lon  输入的经度
 * @return     裁剪后的经度
 */
qreal Bing::clipLon(qreal lon)
{
    return clip(lon, -180.0, 180);
}

/**
 * @brief      限定纬度范围值,防止超限,经度范围[-85.05112878, 85.05112878]
 * @param lat  输入的纬度
 * @return     裁剪后的纬度
 */
qreal Bing::clipLat(qreal lat)
{
    return clip(lat, -85.05112878, 85.05112878);
}

/**
 * @brief       根据输入的瓦片级别计算全地图总宽高,适用于墨卡托投影
 * @param level 1-23(bing地图没有0级别,最低级别为1,由4块瓦片组成)
 * @return      以像素为单位的地图宽度和高度。
 */
uint Bing::mapSize(int level)
{
    uint w = 256;   // 第0级别为256*256
    return (w << level);
}

/**
 * @brief        计算指定纬度、级别的地面分辨率(不同纬度分辨率不同)
 * @param lat    纬度
 * @param level  地图级别 1-23(bing地图没有0级别,最低级别为1,由4块瓦片组成)
 * @return       地面分辨率 单位(米/像素)
 */
qreal Bing::groundResolution(qreal lat, int level)
{
    lat = clipLat(lat);
    return qCos(lat * M_PI / 180) * 2 * M_PI * g_EarthRadius / mapSize(level);
}

/**
 * @brief           计算地图比例尺,地面分辨率和地图比例尺也随纬度而变化
 * @param lat       纬度
 * @param level     地图级别 1-23(bing地图没有0级别,最低级别为1,由4块瓦片组成)
 * @param screenDpi 屏幕分辨率,单位为点/英寸  通常为 96 dpi
 * @return          地图比例尺 1:N(地图上1厘米表示实际N厘米)
 */
qreal Bing::mapScale(qreal lat, int level, int screenDpi)
{
    return groundResolution(lat, level) * screenDpi / 0.0254;   // 1英寸等于0.0254米
}

/**
 * @brief         将一个点从纬度/经度WGS-84墨卡托坐标(以度为单位)转换为指定细节级别的像素XY坐标。
 * @param lon     经度
 * @param lat     纬度
 * @param level   地图级别
 * @return        像素坐标
 */
QPoint Bing::latLongToPixelXY(qreal lon, qreal lat, int level)
{
    lon = clipLon(lon);
    lat = clipLat(lat);

    qreal x = (lon + 180) / 360;
    qreal sinLat = qSin(lat * M_PI / 180);
    qreal y = 0.5 - qLn((1 + sinLat) / (1 - sinLat)) / (4 * M_PI);

    uint size = mapSize(level);
    qreal pixelX = x * size + 0.5;
    pixelX = clip(pixelX, 0, size - 1);
    qreal pixelY = y * size + 0.5;
    pixelY = clip(pixelY, 0, size - 1);

    return QPoint(pixelX, pixelY);
}

/**
 * @brief         将像素从指定细节级别的像素XY坐标转换为经纬度WGS-84坐标(以度为单位)
 * @param pos    像素坐标
 * @param level
 * @param lon
 * @param lat
 */
void Bing::pixelXYToLatLong(QPoint pos, int level, qreal& lon, qreal& lat)
{
    uint size = mapSize(level);
    qreal x = (clip(pos.x(), 0, size - 1) / size) - 0.5;
    qreal y = 0.5 - (clip(pos.y(), 0, size - 1) / size);
    lon = x * 360;
    lat = 90 - (360 * qAtan(qExp(-y * 2 * M_PI)) / M_PI);
}

/**
 * @brief     像素坐标转瓦片编号
 * @param pos  像素坐标
 * @return    瓦片编号
 */
QPoint Bing::pixelXYToTileXY(QPoint pos)
{
    int x = pos.x() / 256;
    int y = pos.y() / 256;
    return QPoint(x, y);
}

/**
 * @brief       瓦片编号转像素坐标
 * @param tile  瓦片编号
 * @return      像素坐标
 */
QPoint Bing::tileXYToPixelXY(QPoint tile)
{
    int x = tile.x() * 256;
    int y = tile.y() * 256;
    return QPoint(x, y);
}

/**
 * @brief       经纬度转瓦片编号
 * @param lon
 * @param lat
 * @param level
 * @return
 */
QPoint Bing::latLongToTileXY(qreal lon, qreal lat, int level)
{
    return pixelXYToTileXY(latLongToPixelXY(lon, lat, level));
}

/**
 * @brief         瓦片编号转经纬度
 * @param tile
 * @param level
 * @return       经纬度 x:经度  y纬度
 */
QPointF Bing::tileXYToLatLong(QPoint tile, int level)
{
    qreal lon = 0;
    qreal lat = 0;
    QPoint pos = tileXYToPixelXY(tile);
    pixelXYToLatLong(pos, level, lon, lat);
    return QPointF(lon, lat);
}

/**
 * @brief         瓦片编号转 bing请求的QuadKey
 * @param tile   瓦片编号
 * @param level  瓦片级别
 * @return
 */
QString Bing::tileXYToQuadKey(QPoint tile, int level)
{
    QString key;
    for (int i = level; i > 0; i--)
    {
        char digit = '0';
        int mask = 1 << (i - 1);
        if ((tile.x() & mask) != 0)
        {
            digit++;
        }
        if ((tile.y() & mask) != 0)
        {
            digit += 2;
        }
        key.append(digit);
    }
    return key;
}

/**
 * @brief            将一个QuadKey转换为瓦片XY坐标。
 * @param quadKey
 * @param tileX      返回瓦片X编号
 * @param tileY      返回瓦片Y编号
 * @param level      返回瓦片等级
 */
void Bing::quadKeyToTileXY(QString quadKey, int& tileX, int& tileY, int& level)
{
    tileX = 0;
    tileY = 0;
    level = quadKey.count();
    QByteArray buf = quadKey.toUtf8();
    for (int i = level; i > 0; i--)
    {
        int mask = 1 << (i - 1);
        switch (buf.at(i - 1))
        {
        case '0':
            break;
        case '1':
            tileX |= mask;
            break;
        case '2':
            tileY |= mask;
            break;
        case '3':
            tileX |= mask;
            tileY |= mask;
            break;
        default:
            break;
        }
    }
}

mapgraphicsview.h文件

#ifndef MAPGRAPHICSVIEW_H
#define MAPGRAPHICSVIEW_H

#include "mapStruct.h"
#include <QGraphicsView>

class MapGraphicsView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MapGraphicsView(QWidget* parent = nullptr);
    ~MapGraphicsView() override;

    void setRect(QRect rect);
    void drawImg(const ImageInfo& info);
    void clear();

signals:
    void updateImage(const ImageInfo& info);   // 添加瓦片图
    void zoom(bool flag);                      // 缩放 true:放大
    void showRect(QRect rect);
    void mousePos(QPoint pos);

protected:
    void mouseMoveEvent(QMouseEvent* event) override;
    void wheelEvent(QWheelEvent* event) override;

private:
    void getShowRect();   // 获取显示范围

private:
    QGraphicsScene* m_scene = nullptr;
    QPointF m_pos;
    QPointF m_scenePos;
};

#endif   // MAPGRAPHICSVIEW_H

mapgraphicsview.cpp文件

#include "mapgraphicsview.h"

#include "bingformula.h"
#include <QDebug>
#include <QGraphicsItem>
#include <QMouseEvent>
#include <QScrollBar>
#include <QWheelEvent>

MapGraphicsView::MapGraphicsView(QWidget* parent)
    : QGraphicsView(parent)
{
    m_scene = new QGraphicsScene();
    this->setScene(m_scene);
    this->setDragMode(QGraphicsView::ScrollHandDrag);   // 鼠标拖拽
    this->setMouseTracking(true);                       // 开启鼠标追踪

    connect(this, &MapGraphicsView::updateImage, this, &MapGraphicsView::drawImg);
}

MapGraphicsView::~MapGraphicsView() {}

/**
 * @brief       缩放后设置场景大小范围
 * @param rect
 */
void MapGraphicsView::setRect(QRect rect)
{
    m_scene->setSceneRect(rect);

    // 将显示位置移动到缩放之前的位置
    this->horizontalScrollBar()->setValue(qRound(m_scenePos.x() - m_pos.x()));
    this->verticalScrollBar()->setValue(qRound(m_scenePos.y() - m_pos.y()));
    getShowRect();
}

/**
 * @brief       绘制瓦片图
 * @param info
 */
void MapGraphicsView::drawImg(const ImageInfo& info)
{
    // 绘制瓦片图
    auto item = m_scene->addPixmap(info.img);
    QPoint pos = Bing::tileXYToPixelXY(QPoint(info.x, info.y));
    item->setPos(pos);
    // 绘制边框
    auto itemR = m_scene->addRect(0, 0, 255, 255, QPen(Qt::red));
    itemR->setPos(pos);
}

/**
 * @brief 清空所有瓦片
 */
void MapGraphicsView::clear()
{
    m_scene->clear();
}

/**
 * @brief        获取鼠标移动坐标
 * @param event
 */
void MapGraphicsView::mouseMoveEvent(QMouseEvent* event)
{
    QGraphicsView::mouseMoveEvent(event);

    emit mousePos(this->mapToScene(event->pos()).toPoint());
    getShowRect();
}

/**
 * @brief        鼠标滚轮缩放
 * @param event
 */
void MapGraphicsView::wheelEvent(QWheelEvent* event)
{
    m_pos = event->pos();                          // 鼠标相对于窗口左上角的坐标
    m_scenePos = this->mapToScene(event->pos());   // 鼠标在场景中的坐标
    if (event->angleDelta().y() > 0)
    {
        m_scenePos = m_scenePos * 2;   // 放大
        emit this->zoom(true);
    }
    else
    {
        m_scenePos = m_scenePos / 2;   // 缩小
        emit this->zoom(false);
    }
}

/**
 * @brief 获取当前场景的显示范围(场景坐标系)
 */
void MapGraphicsView::getShowRect()
{
    QRect rect;
    rect.setTopLeft(this->mapToScene(0, 0).toPoint());
    rect.setBottomRight(this->mapToScene(this->width(), this->height()).toPoint());
    emit this->showRect(rect);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值