地图下载核心,如何使用Python下载高德地图瓦片并拼接成大图?
WMTS地图瓦片
WMTS (Web Map Tile Service) 是一种提供基于瓦片的地图数据服务的网络协议。它通过将地图数据分割成相同大小的瓦片,并预先生成这些瓦片以供客户端请求,从而提高了地图数据呈现速度和效率。WMTS使用RESTful Web服务和基于HTTP/HTTPS协议进行数据交换,可通过Web浏览器、GIS软件和其他客户端应用程序访问和使用。WMTS广泛用于Web地图、桌面应用程序、移动应用程序等。 它是Open Geospatial Consortium(OGC)发布的开放标准之一。
Requst请求
下面我们对高德地图瓦片进行分析,瓦片是通过X,Y,Z形式的行列号来请求一张图片的方式获取瓦片数据的,请求返回的数据为一个进制的图片数据,所以我们要建立一个图片请求,并将请求到的图片返回。
request请求并返回瓦片代码:
def request_tile(self, tile_url):
headers = {
"Connection": "keep-alive",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Cookie":
"HWWAFSESTIME=1681889500446; HWWAFSESID=53a7d799024cfb3f627"
}
blank_img = Image.new(mode="RGB", size=(256, 256))
try:
r = requests.get(url=tile_url, headers=headers, timeout=20)
if r.status_code == 200:
#将请求的内容转为图片
img = Image.open(BytesIO(r.content))
return img
else:
return blank_img
except:
print("failed")
return blank_img
影像瓦片与道路瓦片合并
在我们使用的示例下载地图数据(高德地图)中,影像数据包括:影像地图、标签地图,两者结合才是我们所看到的影像地图,标签地图与影像地图的请求方式一样,为一张256*256的透明背景PNG。
下载影像地图的时候,我们要将标签地图叠加在影像地图之上,下面代码中vec_tile_url
为影像地图地址,cta_tile_url
为标签地图地址,xyz是瓦片的行列号,我们根据传入的行列号与地图级别对瓦片进行下载并重叠。
def get_tile(self, col, row, zoom):
vec_tile_url = "https://webst01.is.autonavi.com/appmaptile?style=6&x={}&y={}&z={}".format(
col, row, zoom)
cta_tile_url = "https://wprd03.is.autonavi.com/appmaptile?x={}&y={}&z={}&lang=zh_cn&size=1&scl=1&style=8<ype=11".format(
col, row, zoom)
vec_img = self.request_tile(vec_tile_url)
cta_img = self.request_tile(cta_tile_url)
new_vec_img = Image.new(mode="RGBA", size=(256, 256))
new_vec_img.paste(vec_img, box=(0, 0))
new_cta_img = Image.new(mode="RGBA", size=(256, 256))
new_cta_img.paste(cta_img, box=(0, 0))
merge_img = Image.alpha_composite(new_vec_img, new_cta_img)
return merge_img
行列号的计算
在下面计算瓦片行列号的时候我们传入的参数为经度(l)纬度(b)级别(zoom),由于我们要下载的瓦片数据为墨卡托投影坐标,所以在计算之前我们要先将经纬度坐标通过lonLat2Mercator
方法转换为默卡托投影坐标,grithHalf:
20037508.3427892(地球周长的一半)。
# 获取杆塔编号
def getTileNum(self, l, b, zoom):
num = int(math.pow(2, zoom))
cellsize = self.grithHalf * 2 / num
np = self.lonLat2Mercator(l, b)
x = math.floor((np.x() + self.grithHalf) / cellsize)
x = max(0, x)
x = min(x, int(num - 1))
y = math.floor((self.grithHalf - np.y()) / cellsize)
y = max(0, y)
y = min(y, int(num - 1))
return x, y
#经纬度转墨卡托投影
def lonLat2Mercator(self, l, b):
if (b > 85.0511287798066):
b = 85.0511287798066
if (b < -85.0511287798066):
b = -85.0511287798066
x = l * self.grithHalf / 180
y = math.log(math.tan((90 + b) * math.pi / 360)) / (math.pi / 180)
y = y * self.grithHalf / 180
return QPointF(x, y)
根据给定坐标范围下载地图
在下载地图时,我们要先给定一个坐标范围,这个范围为地图的左下角坐标及右上角坐标,如:[99,20,105,40];然后通过范围计算出我们瓦片在X方向上的最小瓦片编号与最大瓦片编号,以及在Y方向上的最小瓦片编号与最大瓦片编号,这样我们就可以得到整个范围的瓦片编号,然后对瓦片进行请求合并导出。
由于瓦片编号在X方向是由左向右递增,在Y方向上是由上到下递增,所以我们在取瓦片X与Y的最大最小编号时要以左上角和右下角的坐标来进行瓦片编号计算。
def download_img(self,minx,miny,maxx,maxy,zoom):
#左上角
nx1, ny1 = self.getTileNum(minx, maxy, zoom)
#右下角
nx2, ny2 = self.getTileNum(maxx, miny, zoom)
print(nx1, ny1, zoom)
print(nx2, ny2, zoom)
tile_w = int(qAbs(nx2 - nx1) * 256 + 256)
tile_h = int(qAbs(ny2 - ny1) * 256 + 256)
new_img = Image.new(mode="RGBA", size=(tile_w, tile_h))
#瓦片总数
total_count = (nx2 - nx1 + 1) * (ny2 - ny1 + 1)
current_count = 0
i = 0
for col in range(nx1, nx2 + 1):
j = 0
for row in range(ny1, ny2 + 1):
t_img = self.get_tile(col, row, zoom)
new_img.paste(t_img, box=(i * 256, j * 256))
current_count += 1
progress = qAbs(current_count / total_count) * 100
print("\r Downloading File Map [{:.0f}%] ".format(progress),
"\r",
end='')
j += 1
i += 1
return new_img
导出图片
mapImage = download_img(103.563246,29.632548,103.686545,29.663265,16)
mapImage.save("D:\\crop_result.png", "PNG")
结果图片展示
完整代码:
import math
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import requests
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
import numpy as np
class MapDownloader(object):
def __init__(self):
self.grithHalf = 20037508.3427892
#经纬度转墨卡托投影
def lonLat2Mercator(self, l, b):
if (b > 85.0511287798066):
b = 85.0511287798066
if (b < -85.0511287798066):
b = -85.0511287798066
x = l * self.grithHalf / 180
y = math.log(math.tan((90 + b) * math.pi / 360)) / (math.pi / 180)
y = y * self.grithHalf / 180
return QPointF(x, y)
# 获取杆塔编号
def getTileNum(self, l, b, zoom):
num = int(math.pow(2, zoom))
cellsize = self.grithHalf * 2 / num
np = self.lonLat2Mercator(l, b)
x = math.floor((np.x() + self.grithHalf) / cellsize)
x = max(0, x)
x = min(x, int(num - 1))
y = math.floor((self.grithHalf - np.y()) / cellsize)
y = max(0, y)
y = min(y, int(num - 1))
return x, y
def download_img(self,minx,miny,maxx,maxy,zoom):
nx1, ny1 = self.getTileNum(minx, maxy, zoom)
nx2, ny2 = self.getTileNum(maxx, miny, zoom)
print(nx1, ny1, zoom)
print(nx2, ny2, zoom)
tile_w = int(qAbs(nx2 - nx1) * 256 + 256)
tile_h = int(qAbs(ny2 - ny1) * 256 + 256)
new_img = Image.new(mode="RGBA", size=(tile_w, tile_h))
#瓦片总数
total_count = (nx2 - nx1 + 1) * (ny2 - ny1 + 1)
current_count = 0
i = 0
for col in range(nx1, nx2 + 1):
j = 0
for row in range(ny1, ny2 + 1):
t_img = self.get_tile(col, row, zoom)
new_img.paste(t_img, box=(i * 256, j * 256))
current_count += 1
progress = qAbs(current_count / total_count) * 100
print("\r Downloading File Map [{:.0f}%] ".format(progress),
"\r",
end='')
j += 1
i += 1
return new_img
def get_tile(self, col, row, zoom):
vec_tile_url = "https://webst01.is.autonavi.com/appmaptile?style=6&x={}&y={}&z={}".format(
col, row, zoom)
cta_tile_url = "https://wprd03.is.autonavi.com/appmaptile?x={}&y={}&z={}&lang=zh_cn&size=1&scl=1&style=8<ype=11".format(
col, row, zoom)
vec_img = self.request_tile(vec_tile_url)
cta_img = self.request_tile(cta_tile_url)
new_vec_img = Image.new(mode="RGBA", size=(256, 256))
new_vec_img.paste(vec_img, box=(0, 0))
new_cta_img = Image.new(mode="RGBA", size=(256, 256))
new_cta_img.paste(cta_img, box=(0, 0))
merge_img = Image.alpha_composite(new_vec_img, new_cta_img)
return merge_img
def request_tile(self, tile_url):
headers = {
"Connection": "keep-alive",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Cookie":
"HWWAFSESTIME=1681889500446; HWWAFSESID=53a7d799024cfb3f627"
}
blank_img = Image.new(mode="RGB", size=(256, 256))
try:
r = requests.get(url=tile_url, headers=headers, timeout=20)
# print(r.status_code)
if r.status_code == 200:
img = Image.open(BytesIO(r.content))
return img
else:
return blank_img
except:
print("failed")
return blank_img
mapDownloader = MapDownloader()
mapImage = mapDownloader.download_img(103.563246,29.632548,103.686545,29.663265,16)
mapImage.save("D:\\crop_result.png", "PNG")
那么,这里一个可以按照地图范围与级别下载地图的方法就做好了;如果大家觉得我的方法还有用的话,也请大家能够点赞鼓励一下,您的鼓励是我分享的动力,谢谢各位!