开发 需要卫星地图作为底图。百度、高德、谷歌、arcgis都提供在线服务,但在不能访问在线服务的地方就不适用了。
网上有不少地图下载器,BigeMap、水经注、太乐、91卫图、Google Maps Downloader等等,但是它们都不是完全免费的(免费版有水印,或限制功能),想要授权就得买或者帮着推广。
于是自己编程实现。
各服务商既然提供在线服务,就必然有取得瓦片的方式。
以谷歌(可能为了和国内服务商提供的道路图层叠加,谷歌的卫星影像在国内是偏移的)为例:
http://mt1.google.cn/vt/lyrs=s&hl=zh-CN&x=3&y=1&z=2&s=Gali
瓦片大小为:256x256。
lyrs 表示的是图层类型,即瓦片类型,具体含义如下:
m:路线图
t:地形图
p:带标签的地形图
s:卫星图
y:带标签的卫星图
h:标签层(路名、地名等)
x , y 是瓦片坐标系的坐标值,z代表缩放级别
x 瓦片的横向索引,起始位置为最左边,数值为0,向右+1递增。
y 瓦片的纵向索引,起始位置为最上面,数值为0,向下+1递增。
z 地图的级别,以Google为例,最上一级为0,向下依次递增。
地图切图方式
一幅地图由4^n个256的正方形组成,n为级别。
例如:第0级为4^0个,即世界地图由一个256图片表示。
第1级世界地图应由4^1 = 4个256图片组成,即世界地图等分成4块256图片。
往下每一级依此类推……
Web墨卡托投影是 互联网地图通用的地图投影方式,将椭圆形地图投影成平面的正方形。
Bounds(地图范围):
[ -20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892],单位为米,20037508.3427892表示地图周长的一半,以地图中心点做为(0,0)坐标。
Levels:地图的级别,例如:0……22。
Resolutions:
分辨率数组,与级别相对应,即一个级别对应一个分辨率,分辨率表示当前级别下单个像素代表的地理长度。
Resolutions[n] = 20037508.3427892 * 2 / 256 / (2^n)
Center:地图显示中心点。
Level:地图显示级别。
viewSize:地图控件窗口的大小。
根据已知地图中心点、显示级别可以将地图显示范围计算出来:
viewBounds = [Center.x - Resolutions[l]*viewSize.width/2,
Center.y - Resolutions[l]*viewSize.height/2,
Center.x + Resolutions[l].viewSize.width/w,
Center.y + Resolutions[l].viewSize.height/h]
使用python爬取地图瓦片,核心逻辑主要分为以下几步:
确定下载的级别,经纬度范围(可以通过百度坐标拾取获得)
如果是全球范围,设为[ -180, 90, 180, -90 ] 即可。
计算出这个范围内瓦片的起始和终止行列号
根据行列号拼接出瓦片的url地址
下载保存图片
先导入依赖包和初始化全局参数。
import math
from math import floor, pi, log, tan, atan, exp
from threading import Thread, Lock
import urllib.request as ur
import PIL.Image as pil
import io
import random
import os
MAP_URLS = {
"arcgis": "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
"google": "http://mt1.google.cn/vt/lyrs={style}&hl=zh-CN&gl=CN&src=app&x={x}&y={y}&z={z}&s=Gali"}
agents = [
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5',
'Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9',
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7',
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1']
# 根据WGS-84的经纬度获取地图中的瓦片坐标
def wgs84_to_tile(lon, lat, zoom):
isnum = lambda x: isinstance(x, int) or isinstance(x, float)
if not (isnum(lon) and isnum(lat)):
raise TypeError("lon and lat must be int or float!")
if not isinstance(zoom, int) or zoom < 0 or zoom > 22:
raise TypeError("zoom must be int and between 0 to 22.")
if lon < 0:
lon = 180 + lon
else:
lon += 180
lon /= 360 # make lon to (0,1)
lat = 85.0511287798 if lat > 85.0511287798 else lat
lat = -85.0511287798 if lat < -85.0511287798 else lat
lat = log(tan((90 + lat) * pi / 360)) / (pi / 180)
lat /= 180 # make lat to (-1,1)
lat = 1 - (lat + 1) / 2 # make lat to (0,1) and left top is 0-point
num = 2 ** zoom
x = floor(lon * num)
x = x - 1 if x > 0 else x
y = floor(lat * num)
return x, y
# 生成访问地图瓦片的URL。
# style: m-地图;s-卫星图
def geturl(source, x, y, z, style):
if source == 'arcgis':
furl = MAP_URLS["arcgis"].format(x=x, y=y, z=z)
elif source == 'google':
furl = MAP_URLS["google"].format(x=x, y=y, z=z, style=style)
else:
raise Exception("Unknown Map Source ! ")
return {'url': furl, 'x': x, 'y': y, 'z': z}
# 多线程下载器
class Downloader(Thread):
# 构造函数
# index 表示第几个线程,count 表示线程的总数
# urls 代表需要下载url列表,root_dir代表下载文件存储根路径
# update 表示每下载一个成功就进行的回调函数
def __init__(self, index, count, urls, root_dir, update):
super().__init__()
self.urls = urls
self.index = index
self.count = count
self.update = update
self.root_dir = root_dir
# 根据url下载瓦片,按照z/x/y.png存储到指定目录
def download(self, url_dict, root_dir):
url = url_dict['url']
x = url_dict['x']
y = url_dict['y']
z = url_dict['z']
path = root_dir + "\\" + str(z) + "\\" + str(x)
file = root_dir + "\\" + str(z) + "\\" + str(x) + "\\" + str(y) + ".png"
if os.path.exists(file):
return
req = ur.Request(url)
err = 0
while (err < 3):
try:
req.add_header('User-Agent', random.choice(agents)) # 换用随机的请求头
data = ur.urlopen(req, timeout=60).read()
except Exception as e:
# print(repr(e) + url)
err += 1
else:
try:
if not os.path.exists(path):
os.makedirs(path)
except:
print("")
f = open(file, 'wb')
f.write(data)
f.close()
return
print("Bad network link." + url)
# raise Exception("Bad network link." + url)
def run(self):
for i, url in enumerate(self.urls):
if i % self.count != self.index:
continue
self.download(url, self.root_dir)
if mutex.acquire():
self.update()
mutex.release()
def down_pics(urls, root_dir, multi=10):
def make_update(s):
def up():
global COUNT
COUNT += 1
print("\b" * 45, end='')
print("DownLoading ... [{0}/{1}]".format(COUNT, s), end='')
return up
url_len = len(urls)
if multi < 1 or multi > 20 or not isinstance(multi, int):
raise Exception("multi of Downloader shuold be int and between 1 to 20.")
tasks = [Downloader(i, multi, urls, root_dir, make_update(url_len)) for i in range(multi)]
for i in tasks:
i.start()
for i in tasks:
i.join()
核心爬取函数
# 左上角的经度、纬度,右下角的经度、纬度,缩放级别,地图源,输出路径,影像类型(默认为卫星图)
def get_pic(x1, y1, x2, y2, z, source, out_path, style='s'):
pos1x, pos1y = wgs84_to_tile(x1, y1, z)
pos2x, pos2y = wgs84_to_tile(x2, y2, z)
lenx = pos2x - pos1x + 1
leny = pos2y - pos1y + 1
print("Total number:{x} X {y}".format(x=lenx, y=leny))
urls = [geturl(source, i, j, z, style) for j in range(pos1y, pos1y + leny) for i in range(pos1x, pos1x + lenx)]
print("urls Created!")
down_pics(urls, out_path, 5) # 使用线程个数
print("\nDownload Finished!")
爬取级别为1的世界地图
get_pic(-180, 90, 180, -90, 1, "arcgis", "D:/word")
爬取后的瓦片数据,可以存储到tomcat下(记得开启跨域设置)。
在Cesium中,imageryProvider使用UrlTemplateImageryProvider。它的url可以使用自定义的{x},{y},{z} 方式,比较灵活。
var viewer = new Cesium.Viewer('cesiumContainer',{
imageryProvider: new Cesium.UrlTemplateImageryProvider({
url: 'http://127.0.0.1:8080/word/{z}/{x}/{y}.png',
fileExtension: 'png'
})
});