Cesium - 地图下载器(python)

开发 需要卫星地图作为底图。百度、高德、谷歌、arcgis都提供在线服务,但在不能访问在线服务的地方就不适用了。

网上有不少地图下载器,BigeMap、水经注、太乐、91卫图、Google Maps Downloader等等,但是它们都不是完全免费的(免费版有水印,或限制功能),想要授权就得买或者帮着推广。

于是自己编程实现。

一、基础知识

1、地图瓦片

各服务商既然提供在线服务,就必然有取得瓦片的方式。

以谷歌(可能为了和国内服务商提供的道路图层叠加,谷歌的卫星影像在国内是偏移的)为例:
http://mt1.google.cn/vt/lyrs=s&hl=zh-CN&x=3&y=1&z=2&s=Gali
在这里插入图片描述
瓦片大小为:256x256。

  1. lyrs 表示的是图层类型,即瓦片类型,具体含义如下:

     m:路线图
     t:地形图
     p:带标签的地形图
     s:卫星图
     y:带标签的卫星图
     h:标签层(路名、地名等)
    
  2. x , y 是瓦片坐标系的坐标值,z代表缩放级别
    x 瓦片的横向索引,起始位置为最左边,数值为0,向右+1递增。
    y 瓦片的纵向索引,起始位置为最上面,数值为0,向下+1递增。
    z 地图的级别,以Google为例,最上一级为0,向下依次递增。

  3. 地图切图方式
    一幅地图由4^n个256的正方形组成,n为级别。
    例如:第0级为4^0个,即世界地图由一个256图片表示。
    第1级世界地图应由4^1 = 4个256图片组成,即世界地图等分成4块256图片。
    往下每一级依此类推……

2、Web墨卡托投影

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实现

使用python爬取地图瓦片,核心逻辑主要分为以下几步:

  1. 确定下载的级别,经纬度范围(可以通过百度坐标拾取获得)
    如果是全球范围,设为[ -180, 90, 180, -90 ] 即可。

  2. 计算出这个范围内瓦片的起始和终止行列号

  3. 根据行列号拼接出瓦片的url地址

  4. 下载保存图片

1、初始化

先导入依赖包和初始化全局参数。

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']

2、经纬度转瓦片坐标

# 根据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

3、根据瓦片坐标拼接出瓦片的url地址

# 生成访问地图瓦片的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}

4、 多线程下载器

# 多线程下载器
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()

5、启动多线程下载

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()

6、爬取主函数

核心爬取函数

# 左上角的经度、纬度,右下角的经度、纬度,缩放级别,地图源,输出路径,影像类型(默认为卫星图)
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!")

7、调用例

爬取级别为1的世界地图

get_pic(-180, 90, 180, -90, 1, "arcgis", "D:/word")

三、在Cesium中的使用

  1. 爬取后的瓦片数据,可以存储到tomcat下(记得开启跨域设置)。

  2. 在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'
    })
});
  1. 显示画面

在这里插入图片描述

  • 1
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值