ETJava Beta | Java    注册   登录
  • 搜索:
  • C++(Qt)-GIS开发-简易瓦片地图下载器

    发表于      阅读(1)     博客类别:Crawler     转自:https://www.cnblogs.com/IntelligencePointer/p/18287298
    如有侵权 请联系我们删除  (页面底部联系我们)  

    Qt-GIS开发-简易瓦片地图下载器

    更多精彩内容
    个人内容分类汇总
    GIS开发

    1、概述

    1. 支持单线程、多线程下载瓦片地图。
    2. 使用QNetworkAccessManager、QNetworkReply实现http、https下载功能;
    3. 支持下载多样式arcGis瓦片地图;
    4. 支持下载多样式高德瓦片地图;
    5. 支持多样式Bing地图下载;
    6. Qt中https下载功能需要安装openssl库。
    7. 本文中不会详细说瓦片地图的原理,写得好的文章太多了。

    开发环境说明

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

    2、安装openssl

    • qt使用QNetworkReply/https下载瓦片地图需要ssl支持,qt默认是没有ssl库的;

    • 使用下列代码打印qt版本支持的ssl版本;

      qDebug() << "输出当前QT支持的openSSL版本: " << QSslSocket::sslLibraryBuildVersionString();
      qDebug() << "OpenSSL支持情况: " <<QSslSocket::supportsSsl();
      qDebug() << "OpenSSL运行时SSL库版本: " << QSslSocket::sslLibraryBuildVersionString();
      
    • windows可以下载对应版本的openssl,然后进行安装(轻量级就可以);

    • linux可以通过命令行安装,也可以下载源码自己编译。

    • openssl的github仓库

    • openssl官网

    • 安装后将openssl/bin文件夹下的libcrypto-1_1-x64.dll、libssl-1_1-x64.dll两个动态库拷贝到qt的编译器路径下,注意区分32和64位

      • D:\Qt\Qt5.14.2\5.14.2\msvc2017_64\bin
      • D:\Qt\Qt5.14.2\5.14.2\mingw73_64\bin

    3、实现效果

    1. 无需注册、无需key进行瓦片地图下载;
    2. 地址可能会失效;
    3. 大量下载可能会限速;
    4. 仅作为学习使用。

    4、主要代码

    • 项目文件结构

    4.1 算法函数

    • 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;
              }
          }
      }
      
      

    4.2 瓦片地图下载url拼接

    • mapinput.h

      #ifndef MAPINPUT_H
      #define MAPINPUT_H
      
      #include <QWidget>
      #include "mapStruct.h"
      
      namespace Ui {
      class MapInput;
      }
      
      class MapInput : public QWidget
      {
          Q_OBJECT
      
      public:
          explicit MapInput(QWidget *parent = nullptr);
          ~MapInput();
      
          const QList<ImageInfo> &getInputInfo();       // 获取下载地图所需的输入信息
      
      private:
          // ArcGis
          void initArcGis();
          void getArcGisMapInfo();
          // 高德
          void initAMap();
          void getAMapInfo();
          // Bing地图
          void initBing();
          void getBingMapInfo();
      
      private:
          Ui::MapInput *ui;
          QList<ImageInfo> m_infos;                // 保存下载瓦片图片的信息
      };
      
      #endif // MAPINPUT_H
      
      
    • mapinput.cpp

      /********************************************************************
       * 文件名: mapinput.cpp
       * 时间:   2024-01-19 22:22:37
       * 开发者:  mhf
       * 邮箱:   1603291350@qq.com
       * 说明:   生成地图下载的输入信息
       * ******************************************************************/
      #include "mapinput.h"
      #include "bingformula.h"
      #include "formula.h"
      #include "ui_mapinput.h"
      #include <QDebug>
      
      MapInput::MapInput(QWidget* parent)
          : QWidget(parent)
          , ui(new Ui::MapInput)
      {
          ui->setupUi(this);
      
          initArcGis();
          initAMap();
          initBing();
      }
      
      MapInput::~MapInput()
      {
          delete ui;
      }
      
      /**
       * @brief 填入ArcGis下载地图类型
       */
      void MapInput::initArcGis()
      {
          for (int i = 0; i < 23; i++)
          {
              ui->com_z->addItem(QString("%1").arg(i), i);
          }
          ui->com_type->addItem("NatGeo_World_Map");
          ui->com_type->addItem("USA_Topo_Maps ");
          ui->com_type->addItem("World_Imagery");
          ui->com_type->addItem("World_Physical_Map");
          ui->com_type->addItem("World_Shaded_Relief");
          ui->com_type->addItem("World_Street_Map");
          ui->com_type->addItem("World_Terrain_Base");
          ui->com_type->addItem("World_Topo_Map");
          ui->com_type->addItem("Canvas/World_Dark_Gray_Base");
          ui->com_type->addItem("Canvas/World_Dark_Gray_Reference");
          ui->com_type->addItem("Canvas/World_Light_Gray_Base");
          ui->com_type->addItem("Canvas/World_Light_Gray_Reference");
          ui->com_type->addItem("Elevation/World_Hillshade_Dark");
          ui->com_type->addItem("Elevation/World_Hillshade");
          ui->com_type->addItem("Ocean/World_Ocean_Base");
          ui->com_type->addItem("Ocean/World_Ocean_Reference");
          ui->com_type->addItem("Polar/Antarctic_Imagery");
          ui->com_type->addItem("Polar/Arctic_Imagery");
          ui->com_type->addItem("Polar/Arctic_Ocean_Base");
          ui->com_type->addItem("Polar/Arctic_Ocean_Reference");
          ui->com_type->addItem("Reference/World_Boundaries_and_Places_Alternate ");
          ui->com_type->addItem("Reference/World_Boundaries_and_Places");
          ui->com_type->addItem("Reference/World_Reference_Overlay");
          ui->com_type->addItem("Reference/World_Transportation");
          ui->com_type->addItem("Specialty/World_Navigation_Charts");
      
          // 填入下载格式
          ui->com_format->addItem("jpg");
          ui->com_format->addItem("png");
          ui->com_format->addItem("bmp");
      }
      
      /**
       * @brief   计算并返回需要下载的瓦片地图信息
       * @return
       */
      const QList<ImageInfo>& MapInput::getInputInfo()
      {
          m_infos.clear();   // 清除之前的内容
      
          switch (ui->tabWidget->currentIndex())   // 判断是什么类型的地图源
          {
          case 0:   // ArcGis
              {
                  getArcGisMapInfo();   // 计算ArcGis下载信息
                  break;
              }
          case 1:
              {
                  getAMapInfo();   // 计算高德地图下载信息
                  break;
              }
          case 2:
              {
                  getBingMapInfo();   // 计算bing地图下载信息
                  break;
              }
          default:
              break;
          }
      
          qDebug() << "瓦片数:" << m_infos.count();
      
          return m_infos;
      }
      
      /**
       * @brief   通过输入地图信息计算需要下载的瓦片图信息,下载ArcGIS地图,WGS84坐标系,Web墨卡托投影,z y x输入
       */
      void MapInput::getArcGisMapInfo()
      {
          static QString url = "https://server.arcgisonline.com/arcgis/rest/services/%1/MapServer/tile/%2/%3/%4.%5";
      
          int z = ui->com_z->currentData().toInt();
          QString type = ui->com_type->currentText();
          QString format = ui->com_format->currentText();
          QStringList lt = ui->line_LTGps->text().trimmed().split(',');   // 左上角经纬度
          QStringList rd = ui->line_RDGps->text().trimmed().split(',');   // 右下角经纬度
          if (lt.count() != 2 || rd.count() != 2)
              return;                                    // 判断输入是否正确
          int ltX = lonTotile(lt.at(0).toDouble(), z);   // 计算左上角瓦片X
          int ltY = latTotile(lt.at(1).toDouble(), z);   // 计算左上角瓦片Y
          int rdX = lonTotile(rd.at(0).toDouble(), z);   // 计算右下角瓦片X
          int rdY = latTotile(rd.at(1).toDouble(), z);   // 计算右下角瓦片Y
      
          ImageInfo info;
          info.z = z;
          info.format = format;
          for (int x = ltX; x <= rdX; x++)
          {
              info.x = x;
              for (int y = ltY; y <= rdY; y++)
              {
                  info.y = y;
                  info.url = url.arg(type).arg(z).arg(y).arg(x).arg(format);
                  m_infos.append(info);
              }
          }
      }
      
      /**
       * @brief 初始化高德地图下载选项信息
       */
      void MapInput::initAMap()
      {
          for (int i = 1; i < 5; i++)
          {
              ui->com_amapPrefix->addItem(QString("wprd0%1").arg(i));
          }
          for (int i = 1; i < 5; i++)
          {
              ui->com_amapPrefix->addItem(QString("webst0%1").arg(i));
          }
          for (int i = 0; i < 19; i++)
          {
              ui->com_amapZ->addItem(QString("%1").arg(i), i);
          }
          // 语言设置
          ui->com_amapLang->addItem("中文", "zh_cn");
          ui->com_amapLang->addItem("英文", "en");
          // 地图类型
          ui->com_amapStyle->addItem("卫星影像图", 6);
          ui->com_amapStyle->addItem("矢量路网", 7);
          ui->com_amapStyle->addItem("影像路网", 8);        // 支持png透明背景
          ui->com_amapStyle->addItem("卫星+影像路网", 9);   // 支持png透明背景
          // 图片尺寸,只在7 8生效
          ui->com_amapScl->addItem("256x256", 1);
          ui->com_amapScl->addItem("512x512", 2);
      
          // 填入下载格式
          ui->com_amapFormat->addItem("jpg");
          ui->com_amapFormat->addItem("png");
          ui->com_amapFormat->addItem("bmp");
      }
      
      /**
       * @brief 计算高德地图瓦片下载信息
       */
      void MapInput::getAMapInfo()
      {
          static QString url = "https://%1.is.autonavi.com/appmaptile?";
      
          int z = ui->com_amapZ->currentData().toInt();
          QString format = ui->com_amapFormat->currentText();
          QStringList lt = ui->line_LTGps->text().trimmed().split(',');   // 左上角经纬度
          QStringList rd = ui->line_RDGps->text().trimmed().split(',');   // 右下角经纬度
          if (lt.count() != 2 || rd.count() != 2)
              return;                                    // 判断输入是否正确
          int ltX = lonTotile(lt.at(0).toDouble(), z);   // 计算左上角瓦片X
          int ltY = latTotile(lt.at(1).toDouble(), z);   // 计算左上角瓦片Y
          int rdX = lonTotile(rd.at(0).toDouble(), z);   // 计算右下角瓦片X
          int rdY = latTotile(rd.at(1).toDouble(), z);   // 计算右下角瓦片Y
      
          ImageInfo info;
          info.z = z;
          info.format = format;
          int style = ui->com_amapStyle->currentData().toInt();
          int count = 1;
          if (style == 9)
          {
              count = 2;   // 如果是下载卫星图 + 路网图则循环两次
          }
      
          for (int i = 0; i < count; i++)
          {
              if (count == 2)
              {
                  if (i == 0)
                  {
                      style = 6;   // 第一次下载卫星图
                      info.format = "jpg";
                  }
                  else
                  {
                      style = 8;             // 第二次下载路网图
                      info.format = "png";   // 如果同时下载卫星图和路网图则路网图为透明png格式
                  }
              }
              QString tempUrl = url.arg(ui->com_amapPrefix->currentText());                     // 设置域名
              tempUrl += QString("&style=%1").arg(style);                                       // 设置地图类型
              tempUrl += QString("&lang=%1").arg(ui->com_amapLang->currentData().toString());   // 设置语言
              tempUrl += QString("&scl=%1").arg(ui->com_amapScl->currentData().toInt());        // 设置图片尺寸,只在7 8生效
              tempUrl += QString("&ltype=%1").arg(ui->spin_amapLtype->value());                 // 设置图片中的信息,只有 7 8有效
      
              for (int x = ltX; x <= rdX; x++)
              {
                  info.x = x;
                  for (int y = ltY; y <= rdY; y++)
                  {
                      info.url = tempUrl + QString("&x=%1&y=%2&z=%3").arg(x).arg(y).arg(z);
                      info.y = y;
                      m_infos.append(info);
                  }
              }
          }
      }
      
      /**
       * @brief 初始化Bing地图配置
       */
      void MapInput::initBing()
      {
          // 服务器
          for (int i = 0; i < 8; i++)
          {
              ui->com_bingPrefix->addItem(QString("%1").arg(i));
          }
          // 地图语言
          ui->com_bingLang->addItem("中文", "zh-cn");
          ui->com_bingLang->addItem("英语", "en-US");
          // 地图类型
          ui->com_bingType->addItem("卫星地图", "a");
          ui->com_bingType->addItem("普通地图", "r");
          ui->com_bingType->addItem("混合地图", "h");
      
          ui->com_bingCstl->addItem("默认", "w4c");
          ui->com_bingCstl->addItem("白天", "vb");    // 白天道路地图
          ui->com_bingCstl->addItem("夜晚", "vbd");   // 夜晚道路图
          // 瓦片等级
          for (int i = 1; i < 21; i++)
          {
              ui->com_bingZ->addItem(QString("%1").arg(i));
          }
          // 填入下载格式
          ui->com_bingFormat->addItem("jpg");
          ui->com_bingFormat->addItem("png");
          ui->com_bingFormat->addItem("bmp");
      }
      
      /**
       * @brief 计算Bing地图的下载信息(这些url可能会失效,后续会使用其他方式下载)
       *  https://learn.microsoft.com/en-us/bingmaps/rest-services/directly-accessing-the-bing-maps-tiles
       */
      void MapInput::getBingMapInfo()
      {
          //https://r1.tiles.ditu.live.com/tiles/r1321001.png?g=1001&mkt=zh-cn
          //http://dynamic.t2.tiles.ditu.live.com/comp/ch/r1321001.png?it=G,OS,L&mkt=en-us&cstl=w4c&ur=cn
          //http://ecn.t{0}.tiles.virtualearth.net/tiles/{1}{2}.png? g={4}
          //https://t0.dynamic.tiles.ditu.live.com/comp/ch/1320300313132?mkt=zh-CN&ur=CN&it=G,RL&n=z&og=894&cstl=vb
          //https://t1.dynamic.tiles.ditu.live.com/comp/ch/13203012200201?mkt=zh-CN&ur=cn&it=G,RL&n=z&og=894&cstl=vbd
          //https://dynamic.t1.tiles.ditu.live.com/comp/ch/1320300313313?it=Z,TF&L&n=z&key=AvquUWQgfy7VPqHn9ergJsp3Q_EiUft0ed70vZsX0_aqPABBdK07OkwrXWoGXsTG&ur=cn&cstl=vbd
      
      #define USE_URL 1
      #if (USE_URL == 0)
          // https://r1.tiles.ditu.live.com/tiles/r1321001.png?g=1001&mkt=zh-cn
          static QString url = "https://r%1.tiles.ditu.live.com/tiles/%2%3.%4?g=1001&mkt=%5";   // 街道图r支持中文
      #elif (USE_URL == 1)
          // http://dynamic.t2.tiles.ditu.live.com/comp/ch/r1321001.png?it=G,OS,L&mkt=en-us&cstl=w4c&ur=cn
          static QString url = "http://dynamic.t%1.tiles.ditu.live.com/comp/ch/%2%3.%4?it=G,OS,L&mkt=%5&cstl=%6&ur=cn";
      #endif
          int z = ui->com_bingZ->currentText().toInt();
          QStringList lt = ui->line_LTGps->text().trimmed().split(',');   // 左上角经纬度
          QStringList rd = ui->line_RDGps->text().trimmed().split(',');   // 右下角经纬度
          if (lt.count() != 2 || rd.count() != 2)
              return;                                    // 判断输入是否正确
          int ltX = lonTotile(lt.at(0).toDouble(), z);   // 计算左上角瓦片X
          int ltY = latTotile(lt.at(1).toDouble(), z);   // 计算左上角瓦片Y
          int rdX = lonTotile(rd.at(0).toDouble(), z);   // 计算右下角瓦片X
          int rdY = latTotile(rd.at(1).toDouble(), z);   // 计算右下角瓦片Y
      
          QString format = ui->com_bingFormat->currentText();
          ImageInfo info;
          info.z = z;
          info.format = format;
          int prefix = ui->com_bingPrefix->currentIndex();
          QString lang = ui->com_bingLang->currentData().toString();   // 语言
          QString type = ui->com_bingType->currentData().toString();   // 类型
          QString cstl = ui->com_bingCstl->currentData().toString();   // 样式
      
          QPoint point;
          for (int x = ltX; x <= rdX; x++)
          {
              info.x = x;
              point.setX(x);
              for (int y = ltY; y <= rdY; y++)
              {
                  info.y = y;
                  point.setY(y);
                  QString quadKey = Bing::tileXYToQuadKey(point, z);   // 将xy转为quadkey
      #if (USE_URL == 0)
                  info.url = url.arg(prefix).arg(type).arg(quadKey).arg(format).arg(lang);
      #elif (USE_URL == 1)
                  info.url = url.arg(prefix).arg(type).arg(quadKey).arg(format).arg(lang).arg(cstl);
      #endif
                  m_infos.append(info);
              }
          }
      }
      
      

    4.3 多线程下载

    • downloadthreads.h

      #ifndef DOWNLOADTHREADS_H
      #define DOWNLOADTHREADS_H
      
      #include "mapStruct.h"
      #include <QFutureWatcher>
      #include <QObject>
      
      class DownloadThreads : public QObject
      {
          Q_OBJECT
      public:
          explicit DownloadThreads(QObject* parent = nullptr);
          ~DownloadThreads();
      
          // 传入需要下载的瓦片信息
          void getImage(QList<ImageInfo> infos);
          void quit();   // 退出下载
      
      signals:
          void finished(ImageInfo info);   // 返回下载后的瓦片,由于QImage为共享内存,所以传递不需要考虑太多性能
      
      private:
          QFuture<void> m_future;
          QList<ImageInfo> m_infos;
      };
      
      #endif   // DOWNLOADTHREADS_H
      
      
    • downloadthreads.cpp

      /********************************************************************
       * 文件名: downloadthreads.cpp
       * 时间:   2024-03-31 20:32:58
       * 开发者:  mhf
       * 邮箱:   1603291350@qq.com
       * 说明:   多线程下载瓦片地图
       * ******************************************************************/
      #include "downloadthreads.h"
      #include <QtConcurrent>
      #include <qnetworkaccessmanager.h>
      #include <qnetworkreply.h>
      
      static DownloadThreads* g_this = nullptr;
      DownloadThreads::DownloadThreads(QObject *parent) : QObject(parent)
      {
          g_this = this;  // 记录当前 this指针,用于传递信号
      }
      
      DownloadThreads::~DownloadThreads()
      {
          g_this = nullptr;
          quit();
      }
      
      /**
       * @brief       下载瓦片
       * @param info
       * @return
       */
      void getUrl(ImageInfo info)
      {
          QNetworkAccessManager manager;
          QNetworkReply* reply = manager.get(QNetworkRequest(QUrl(info.url)));
          // 等待返回
          QEventLoop loop;
          QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
          loop.exec();
      
          if(reply->error() == QNetworkReply::NoError)
          {
              QByteArray buf = reply->readAll();
              info.img.loadFromData(buf);
          }
          else
          {
              info.count++;
              if(info.count < 3)
              {
                  getUrl(info);   // 下载失败重新下载
                  return;
              }
              else
              {
                  qWarning() << "下载失败:" << reply->errorString();
              }
          }
          if(g_this)
          {
              emit g_this->finished(info);  // 通过信号将下载后的瓦片传出去
          }
      }
      
      /**
       * @brief         调用线程池下载瓦片
       * @param infos
       */
      void DownloadThreads::getImage(QList<ImageInfo> infos)
      {
          m_infos = infos;    // 这里不能使用infos,因为会在函数退出释放
      #if 0   // 由于map使用的是全局线程池,所以可以查看、设置线程数
          qDebug() <<QThreadPool::globalInstance()->maxThreadCount();   // 查看最大线程数
          QThreadPool::globalInstance()->setMaxThreadCount(1);          // 设置最大线程数
      #endif
          m_future = QtConcurrent::map(m_infos, getUrl);
      }
      
      /**
       * @brief 退出下载
       */
      void DownloadThreads::quit()
      {
          if(m_future.isRunning())   // 判断是否在运行
          {
              m_future.cancel();               // 取消下载
              m_future.waitForFinished();      // 等待退出
          }
      }
      
      

    5、源码地址

    6、参考