Qt代码实现地图瓦片电子放大/无极缩放/可将层级拓展到22级别/精确到0.03米
一、前言说明
想实现这个功能由来已久,对标奥维地图或者qgc等地图控件,都具备这个功能,也就是当瓦片不存在的时候,先查找上级瓦片,然后裁切对应区域,最后拉伸填充到当前瓦片区域,看起来会模糊一些,但总归是效果满足人眼的需要,不然就是一个空白区域很突兀。理论上来说,地图放大的时候,上一级的瓦片之前是肯定加载过的,那就可以利用这个机制做文章,有标准公式来计算,比如当前瓦片区域对应x、y、z,他的上级瓦片是x/2、y/2、z-1,然后再根据xy的奇数偶数,来判断需要裁切四个区域的哪个区域,按照这个机制吭哧吭哧一顿输出,搞完收工非常完美,先从内存瓦片中找到对应上级的瓦片,裁切对应区域拉伸填充占位,再去联网下载瓦片,下载好了再覆盖对应区域,整体上看下来就是先一个模糊的瓦片,下载好了替换后就是一个清晰的图片。
光有这个查找上级的功能还是不够的,顺藤摸瓜能不能实现无极缩放呢?或者最少两到三个级别,观察发现市面上的其他地图软件,几乎都是2-3级别,后面发现有通用的公式,传入xy和层级,直接裁切哪个区域的图片返回,2级别是图片的八分之一,3层级就是16分之一,层级越大切割的数量越多,图片月模糊,又是一顿吭哧吭哧搞,实现了无极缩放,用户非常满意,理论上来说,只要提供一张最初的世界地图瓦片,可以无极缩放到所有层级,后面验证下来,我还是太年轻了,超过8个层级,居然用的原始图片裁切的,原来最大只能往上找8个层级,因为第八层级就已经是256等分了,如果是256的瓦片,就是一个像素要填充一个区域,全是马赛克,再小就是0,导致裁切失败返回的原图。
二、效果图



三、相关代码
void LayerMap::draw(QPainter *painter, const RectWorldPx &backbuffer_rect, int cur_zoom) const
{
QReadLocker locker(&m_mutex);
//没有设置或者不可见则不用继续
if (!m_map_adapter || !m_map_image || !isVisible(cur_zoom)) {
return;
}
//没有设置瓦片地址也不用继续
if (m_map_adapter->url().isEmpty()) {
return;
}
//绘制背景颜色/形成样式风格
if (m_bgColor != Qt::transparent) {
painter->fillRect(backbuffer_rect.rawRect(), m_bgColor);
}
int z = cur_zoom;
int size = m_tile_size;
bool offline = m_map_adapter->isOffline();
//计算瓦片绘制区域
int left = qFloor(backbuffer_rect.leftPx() / size);
int top = qFloor(backbuffer_rect.topPx() / size);
int right = qFloor(backbuffer_rect.rightPx() / size);
int bottom = qFloor(backbuffer_rect.bottomPx() / size);
//绘制网格背景/绘制完不用继续
if (ImageHelper::drawTileGrid) {
this->drawGrid(painter, z, size, left, top, right, bottom);
return;
}
//1. 先从内存中查找/找不到则记住XY/意味着需要获取图片
//2. 内存中找不到/逐个上级瓦片电子放大/临时填充区域
//3. 再对XY列表获取图片/本地只查找本地文件/远程只联网获取
//4. 地址队列和标识队列只是为了方便直接传入/不用再计算
//5. scale队列用来存放电子放大的层级/方便下面判断是否找到了电子放大的
//6. image队列用来存放电子放大的图片/下面绘制需要用到/找到了原图则用原图/否则用电子放大的
QList<int> listX;
QList<int> listY;
QList<QUrl> listUrl;
QList<QString> listFlag;
QList<int> listScale;
QList<QPixmap> listImage;
#if 1
//先从内存中查找/从中间往四周扩散/靠近地图焦点的瓦片优先查找和下载
int centerX = (left + right) / 2;
int centerY = (top + bottom) / 2;
//按到中心点的距离分桶/距离近的瓦片优先处理/QMap数据会自动排序
QMap<int, QList<QPoint> > mapTile;
for (int x = left; x <= right; ++x) {
for (int y = top; y <= bottom; ++y) {
int distance = (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY);
mapTile[distance] << QPoint(x, y);
}
}
QMapIterator<int, QList<QPoint> > itTile(mapTile);
while (itTile.hasNext()) {
itTile.next();
foreach (QPoint point, itTile.value()) {
int x = point.x();
int y = point.y();
#else
//先从内存中查找/从左往右从上到下依次查找
for (int x = left; x <= right; ++x) {
for (int y = top; y <= bottom; ++y) {
#endif
if (!m_map_adapter->isTileValid(x, y, z)) {
continue;
}
QUrl url;
QString flag = this->getUrlFlag(url, x, y, z, 0);
QPixmap pixmap = ImageHelper::findPixmap(flag);
if (pixmap.isNull()) {
//循环从内存中的上级瓦片中查找/找到则裁切对应区域/返回放大层级
int scale = this->findParentPixmap(pixmap, x, y, z, size, offline);
//如果还没找到/往下级找四张拼起来/拼接成功则把scale改为255区分上下级
this->findChildPixmap(pixmap, scale, x, y, z, size, offline);
listX << x;
listY << y;
listUrl << url;
listFlag << flag;
listScale << scale;
listImage << pixmap;
} else {
this->drawPixmap(painter, pixmap, size, x, y, 0, offline, false);
}
}
}
//超过最大查找层级不用继续
int level = z - m_tile_zoom;
if (level > ImageHelper::zoomInFindLevel) {
return;
}
//内存中不存在的则主动获取
int count = listX.size();
for (int i = 0; i < count; ++i) {
int x = listX.at(i);
int y = listY.at(i);
QUrl url = listUrl.at(i);
QString flag = listFlag.at(i);
int scale = listScale.at(i);
QPixmap image = listImage.at(i);
//获取绘制用的瓦片图片并绘制/没有原图则取电子放大或者下级拼接的
this->getAndDrawPixmap(painter, url, flag, x, y, z, size, level, scale, image, offline);
}
//预加载瓦片区域往四周扩散部分/方便拖动快速绘制/不用再去临时加载
if (!offline && z <= m_tile_zoom) {
this->prefetchImage(z, left, top, right, bottom, offline);
}
//绘制网格文字
if (ImageHelper::drawTileInfo) {
this->drawInfo(painter, z, size, left, top, right, bottom);
}
}
QString LayerMap::getUrlFlag(QUrl &url, int x, int y, int z, int level) const
{
url = m_map_adapter->tileQuery(x >> level, y >> level, z - level);
return TileHelper::getUrlFlag(url.toString(), m_tile_size, m_epsg_type);
}
int LayerMap::findParentPixmap(QPixmap &pixmap, int x, int y, int z, int size, bool offline) const
{
//要过滤下最小级别不能小于1
int scale = 0;
int maxLevel = qMin(ImageHelper::zoomInFindLevel, z - 1);
for (int level = 1; level <= maxLevel; ++level) {
QUrl url;
QString flag = this->getUrlFlag(url, x, y, z, level);
pixmap = ImageHelper::findPixmap(flag);
//离线状态还需要从文件获取
if (offline && pixmap.isNull()) {
pixmap = m_map_image->getImage(url, flag, offline, false);
}
if (!pixmap.isNull()) {
scale = level;
MapHelper::copyPixmapGrid(pixmap, size, x, y, level);
break;
}
}
return scale;
}
void LayerMap::findChildPixmap(QPixmap &pixmap, int &scale, int x, int y, int z, int size, bool offline) const
{
//如果还没找到/往下级找四张拼起来/当前级一张瓦片对应下一级四张瓦片
if (!offline && ImageHelper::zoomOutFindLevel > 0 && pixmap.isNull()) {
int nextX = x * 2;
int nextY = y * 2;
int nextZ = z + 1;
QPixmap pixs[2][2];
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
QUrl url;
QString flag = this->getUrlFlag(url, nextX + i, nextY + j, nextZ, 0);
QPixmap pix = ImageHelper::findPixmap(flag);
if (!pix.isNull()) {
pixs[i][j] = pix;
} else {
//只要有一张没找到则不用继续
return;
}
}
}
//四张瓦片都找到则拼接成一张大图/正好是当前瓦片的完整区域
//pixs[i][j]中i是x偏移对应列/j是y偏移对应行/按坐标顺序绘制
scale = 255;
MapHelper::mergePixmapGrid(pixmap, size, pixs[0][0], pixs[1][0], pixs[0][1], pixs[1][1]);
}
}
void LayerMap::getAndDrawPixmap(QPainter *painter, QUrl url, QString flag, int x, int y, int z, int size, int level, int scale, const QPixmap &image, bool offline) const
{
//查找上一个层级的瓦片
if (level > 0) {
flag = this->getUrlFlag(url, x, y, z, level);
}
//获取到的上一级瓦片/还需要裁切对应区域
QPixmap pixmap = m_map_image->getImage(url, flag, offline, false);
if (level > 0 && !pixmap.isNull()) {
MapHelper::copyPixmapGrid(pixmap, size, x, y, level);
}
//找到了原图则用原图/没有则取电子放大或者下级拼接的
int level2 = level;
if (scale > 0 && pixmap.isNull()) {
level2 = scale;
pixmap = image;
}
//绘制瓦片到对应网格
this->drawPixmap(painter, pixmap, size, x, y, level2, offline, scale == 0);
}
void LayerMap::drawPixmap(QPainter *painter, QPixmap &pixmap, int size, int x, int y, int level, bool offline, bool fill) const
{
bool isNull = pixmap.isNull();
if (fill && isNull) {
pixmap = offline ? ImageHelper::getNotFoundPixmap(size) : ImageHelper::getLoadingPixmap(size);
}
//绘制瓦片到对应网格/可能是电子放大的图片/需要填充缩放
if (!pixmap.isNull()) {
MapHelper::scalePixmap(pixmap, size);
QPoint point(x * size, y * size);
painter->drawPixmap(point, pixmap);
//绘制查找到的瓦片层级/裁切的图片
if (!isNull && ImageHelper::drawTileInfo) {
QString text = QString::number(level > 0 ? level : 0);
OverlayBaseHelper::drawText(painter, point, text, Qt::green, size, Qt::AlignRight);
}
}
}
void LayerMap::drawGrid(QPainter *painter, int z, int size, int left, int top, int right, int bottom) const
{
static QPixmap pixmap = ImageHelper::getNotFoundPixmap(size);
for (int x = left; x <= right; ++x) {
for (int y = top; y <= bottom; ++y) {
if (m_map_adapter->isTileValid(x, y, z)) {
this->drawPixmap(painter, pixmap, size, x, y);
}
}
}
}
void LayerMap::drawInfo(QPainter *painter, const QPointF &point, int x, int y, int z, int size) const
{
QString text = QString("z=%3\nx=%1\ny=%2").arg(x).arg(y).arg(z);
OverlayBaseHelper::drawText(painter, point, text, Qt::red, size, Qt::AlignLeft | Qt::AlignVCenter);
}
void LayerMap::drawInfo(QPainter *painter, int z, int size, int left, int top, int right, int bottom) const
{
for (int x = left; x <= right; ++x) {
for (int y = top; y <= bottom; ++y) {
if (m_map_adapter->isTileValid(x, y, z)) {
QPoint point(x * size, y * size);
this->drawInfo(painter, point, x, y, z, size);
}
}
}
}
四、相关地址
- 国内站点:https://gitee.com/feiyangqingyun
- 国际站点:https://github.com/feiyangqingyun
- 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 文件地址:https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A 提取码:o05q 文件名:bin_mapwidget.zip
五、功能特点
- 支持各种地图源,包括天地图、高德地图、腾讯地图、谷歌地图、微软地图、电子海图、ArcGis、OpenStreet、360地图等。
- 同时支持WGS-84地球坐标系和GCJ-02火星坐标系,如果是天地图、谷歌地图,自动应用WGS-84坐标系,如果是高德地图、腾讯地图,自动应用GCJ-02坐标系。也可以主动设置在WGS-84坐标系加载高德地图,内部会自动处理坐标偏移。
- 同时支持默卡托投影和经纬度投影,比如天地图的地图源可以是默卡托投影或者经纬度投影。
- 每一种地图源都支持在线和离线两种场景需求,可以自定义在线瓦片地址格式和离线瓦片地址格式。
- 多线程下载和加载瓦片图片文件,多线程绘制,自动缓存瓦片文件。
- 在线模式下,可以开启是否缓存文件,指定缓存路径,将下载的瓦片文件存放到本地,默认优先从缓存文件查找,如果存在缓存文件则加载缓存文件,不存在则联网下载。
- 可以任意拖曳地图,鼠标滚轮放大和缩小地图,以鼠标所在位置作为缩放中心点,提供缩放控件手动单击进行操作,支持鼠标左键双击放大和右键双击缩小。
- 多图层机制,支持多个瓦片叠加图层和图形绘制图层,全部采用双缓冲技术,所有的图形和瓦片全部绘制到一个图片文件上,最终再将图片文件绘制到地图控件。不可见区域的图层包括覆盖物不会触发绘制,最大程度降低CPU占用。
- 动态图层机制,瓦片图层和图形图层分离,图形图层不更新的情况下,动态更新底图瓦片图层,任意切换更新多个瓦片图层。无需重新加载整个地图。
- 预加载机制,默认绘制的图层大小以当前区域往四周放大一倍,这样在鼠标拖动和缩放的时候,不会看到明显的加载过程,因为已经提前预加载好了,体验友好。
- 内置了多种图形覆盖物,包括坐标点、纯文本、标注点、折线条、多边形、矩形、圆形、雷达、点聚合、热力图等,可以设置边框颜色粗细、填充颜色和透明度等众多参数。
- 标注点支持旋转角度和提示文本,其中提示文本可以设置在标注点的相对位置,标注点图片支持gif动图,可以动态切换静态图和动图。
- 标注点和提示文本可以设置相对位置,位置包括左侧、右侧、上侧、下侧、中间、左上角、右上角、左下角、右下角。标注点默认按照底部居中对齐,一般圆形图标可以设置中心点对齐。
- 标注点提示文本可设置背景颜色,透明度、颜色边框和粗细,支持换行和多行文字。
- 所有的图形可以动态更新前景色、颜色粗细、背景颜色、颜色透明度等。
- 支持删除单个图形、删除一种类型的图形、删除所有图形、隐藏单个或者所有图形等。
- 支持动态绘制各种图形,开启后直接在地图上鼠标按下绘制,鼠标右键结束绘制,非常方便快捷。
- 在对应图形区域鼠标按下,发出图形单击信号,精准识别单击区域,比如折线以鼠标在折线条上作为判断依据,多边形区域以鼠标在整个多边形区域内为准,而不是以矩形区域,包括圆形也是以圆形内部为准。
- 可以动态启动禁用比例尺、十字线、缩放控件、地图拖曳、键盘操作、滚轮缩放、双击缩放、鼠标追踪等特性。
- 可以任意指定经纬度区域进行瓦片拼接保存成图片文件,也可以直接对整个可视区域或者缓存区域的地图图片文件保存。支持任意多边形轮廓保存成图片,比如某个行政区的瓦片保存。
- 图形可以动态设置zindex层叠顺序,值越大,越显示在前面,内部维护着一个zindex表,默认按照添加的先后顺序增加,后面添加的显示在前面,主动设置后,按照设置的zindex来绘制。
- 支持将QWidget对象作为覆盖物添加到地图控件中,跟随地图移动位置,极大提高灵活性,比如可以将自定义控件直接作为地图控件的子对象加入进去。
- 支持测距和测面积,动态绘制多边形区域,实时动态调整位置和点坐标自动重新计算,可以关闭单个测距测面的图形。
- 内置MarkerMove轨迹移动类,支持历史轨迹数据回放和实时轨迹移动,可设置图标、轨迹线的颜色和粗细、移动速度、移动间隔、平滑移动、自动中心等,支持多条轨迹线条同时移动。
- 内置MarkerLine航迹规划类,支持动态添加航迹点,显示对应箭头,可以动态拖曳调整航迹点的位置,选中点高亮显示。
- 大量使用按需绘制机制,包括内部提供合理的默认值来触发绘制,也可以手动传入参数指定是否需要立即绘制,比如删除了某个覆盖物,有些频繁的操作可以不指定立即绘制,等操作完成后再统一一起绘制,效率更高。
- 默认开启内存瓦片机制,所有加载过的瓦片文件都存储在内存中,下次再次绘制直接从内存取出来绘制,既不需要从联网获取,也不需要从缓存文件获取,直接内存取出来绘制,响应迅速效率最高体验最佳。
- 支持批量添加覆盖物,比如几万个标注点和圆形,都是瞬间完成绘制,相比web网页的方式,性能提升百倍以上。
- 支持街道图、卫星图、混合图、路网图等各种图层,可以任意叠加N个图层,甚至杂交不同地图厂家的瓦片文件。
- 内置缩放动画效果,在缩放的时候先截取上一个地图画面作为缩放动画基准图片,逐步过渡到下一个层级。
- 内置电子放大机制,瓦片图片不存在的时候,优先获取该区域上一图层的瓦片,按照规则裁切图片缩放填充,支持无极缩放。在线和离线模式都支持电子放大,在线情况下主动获取上一级的瓦片,离线情况下主动本地文件查找。
- 电子放大除了支持找上级瓦片,还支持找下级瓦片,比如地图缩小的时候,先找4张下级瓦片按照规则合并到对应区域瓦片,防止缩小的时候看到空白区域。
- 支持双指捏合缩放,在移动端或者任何可以触摸的地方,双指拉大放大地图,双指聚拢缩小地图,平滑动画过渡缩放。
- 提供离线地图下载模块,多线程高速并发下载,可框选指定区域下载瓦片,可指定任意级别下载。
- 提供高程海拔显示模块,离线识别高程海拔数据,传入经纬度即可,无需引入任何第三方模块。
- 纯QWidget绘制,非qml也非web,不依赖qml或者浏览器控件,没有任何第三方库依赖,支持极低性能的嵌入式环境。
- 原创轻量级,核心代码5000行,整体代码10000行,架构漂亮,注释详细,拓展方便,容易学习,适合各种初学者和进阶者,方便二次开发。
- 支持任意Qt版本、任意系统、任意编译器,包括嵌入式linux和各种国产电脑环境。
- 全部原创自研,古法编程,不含任何AI代码,持续迭代更新,商业级别、品质保证。

浙公网安备 33010602011771号