├── LICENSE ├── README.md └── downloader_gmtchina.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zhengjie9510 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 谷歌与高德地图下载器 2 | 3 | ## 中文: 4 | [downloader_gmtchina.py](https://github.com/CovMat/google-map-downloader/blob/master/downloader_gmtchina.py): 5 | 添加了高德地图下载功能 6 | 7 | ## 指南/Guide 8 | ### 初学者必看 9 | 本下载器是 python 脚本,必须会使用conda和python才能使用本工具。如果你对conda和python一无所知,请先学习这个教程: https://seismo-learn.org/software/conda/ 10 | 。请注意没有学会掌握 conda 那么接下来的所有内容对你来说都没有任何意义! 11 | 12 | ### 安装运行所需的必要软件包 13 | ```shell 14 | conda install -c anaconda numpy pillow py-opencv 15 | conda install -c conda-forge gdal 16 | ``` 17 | ### 下载 18 | 右键另存为下载: https://raw.githubusercontent.com/CovMat/google-map-downloader/master/downloader_gmtchina.py 19 | 20 | ### 使用 21 | 国内的地图服务均进行过GCJ02坐标系加密,因此这个工具对高德地图、高德卫星图与谷歌地图进行了GCJ02到WGS84坐标系的变换。谷歌卫星图则不需要进行变换。 22 | 此外,境内用户需确保能够正常使用谷歌地图网页版才能使用本工具下载谷歌地图。 23 | 24 | 提供如下参数运行脚本,即可下载 tif 格式地图: 25 | ```shell 26 | python downloader_gmtchina.py 起始经度 结束经度 起始纬度 结束纬度 地图层级 输出图片文件名 地图来源 27 | ``` 28 | 地图层级取 0 - 22 之间的整数,值越高分辨率越大。地图来源目前可以选择以下四个: 29 | 30 | |代码|地图来源| 31 | | ---- | ---- | 32 | |amap | 高德地图| 33 | |amap_sat | 高德卫星图| 34 | |google | 谷歌地图| 35 | |google_sat | 谷歌卫星图| 36 | 37 | 以厦门市为例,使用如下参数下载四种地图: 38 | 39 | ```shell 40 | python downloader_gmtchina.py 118.055917 118.244753 24.399450 24.559724 12 google.tif google 41 | python downloader_gmtchina.py 118.055917 118.244753 24.399450 24.559724 16 google_sat.tif google_sat 42 | python downloader_gmtchina.py 118.055917 118.244753 24.399450 24.559724 12 amap.tif amap 43 | python downloader_gmtchina.py 118.055917 118.244753 24.399450 24.559724 16 amap_sat.tif amap_sat 44 | ``` 45 | 46 | ## 问题/Issues 47 | 如果出现 Bad network link 报错,请在代码中修改 HEADERS 再试一次 48 | ```python 49 | def download(self,url): 50 | HEADERS = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36'} 51 | # 如果以上 Header不行,再尝试下面的 Header 52 | # 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE' 53 | header = ur.Request(url,headers=HEADERS) 54 | err=0 55 | while(err<3): 56 | try: 57 | data = ur.urlopen(header).read() 58 | except: 59 | err+=1 60 | else: 61 | return data 62 | raise Exception("Bad network link.") 63 | ``` 64 | -------------------------------------------------------------------------------- /downloader_gmtchina.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | ''' 3 | This code is used to download image from google and amap 4 | 5 | @date : 2020-3-13 6 | @author: Zheng Jie 7 | @E-mail: zhengjie9510@qq.com 8 | 9 | Modified by GMT-china.org 10 | ''' 11 | 12 | import sys 13 | import io 14 | import math 15 | import multiprocessing 16 | import time 17 | import urllib.request as ur 18 | from math import floor, pi, log, tan, atan, exp 19 | from threading import Thread 20 | 21 | import PIL.Image as pil 22 | import cv2 23 | import numpy as np 24 | from osgeo import gdal, osr 25 | 26 | 27 | # ------------------Interchange between WGS-84 and Web Mercator------------------------- 28 | # WGS-84 to Web Mercator 29 | def wgs_to_mercator(x, y): 30 | y = 85.0511287798 if y > 85.0511287798 else y 31 | y = -85.0511287798 if y < -85.0511287798 else y 32 | 33 | x2 = x * 20037508.34 / 180 34 | y2 = log(tan((90 + y) * pi / 360)) / (pi / 180) 35 | y2 = y2 * 20037508.34 / 180 36 | return x2, y2 37 | 38 | 39 | # Web Mercator to WGS-84 40 | def mercator_to_wgs(x, y): 41 | x2 = x / 20037508.34 * 180 42 | y2 = y / 20037508.34 * 180 43 | y2 = 180 / pi * (2 * atan(exp(y2 * pi / 180)) - pi / 2) 44 | return x2, y2 45 | 46 | 47 | # -------------------------------------------------------------------------------------- 48 | 49 | # -----------------Interchange between GCJ-02 to WGS-84--------------------------- 50 | # All public geographic data in mainland China need to be encrypted with GCJ-02, introducing random bias 51 | # This part of the code is used to remove the bias 52 | def transformLat(x, y): 53 | ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * math.sqrt(abs(x)) 54 | ret += (20.0 * math.sin(6.0 * x * math.pi) + 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 55 | ret += (20.0 * math.sin(y * math.pi) + 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 56 | ret += (160.0 * math.sin(y / 12.0 * math.pi) + 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 57 | return ret 58 | 59 | 60 | def transformLon(x, y): 61 | ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * math.sqrt(abs(x)) 62 | ret += (20.0 * math.sin(6.0 * x * math.pi) + 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 63 | ret += (20.0 * math.sin(x * math.pi) + 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 64 | ret += (150.0 * math.sin(x / 12.0 * math.pi) + 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 65 | return ret 66 | 67 | 68 | def delta(lat, lon): 69 | ''' 70 | Krasovsky 1940 71 | // 72 | // a = 6378245.0, 1/f = 298.3 73 | // b = a * (1 - f) 74 | // ee = (a^2 - b^2) / a^2; 75 | ''' 76 | a = 6378245.0 # a: Projection factor of satellite ellipsoidal coordinates projected onto a flat map coordinate system 77 | ee = 0.00669342162296594323 # ee: Eccentricity of ellipsoid 78 | dLat = transformLat(lon - 105.0, lat - 35.0) 79 | dLon = transformLon(lon - 105.0, lat - 35.0) 80 | radLat = lat / 180.0 * math.pi 81 | magic = math.sin(radLat) 82 | magic = 1 - ee * magic * magic 83 | sqrtMagic = math.sqrt(magic) 84 | dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * math.pi) 85 | dLon = (dLon * 180.0) / (a / sqrtMagic * math.cos(radLat) * math.pi) 86 | return {'lat': dLat, 'lon': dLon} 87 | 88 | 89 | def outOfChina(lat, lon): 90 | if (lon < 72.004 or lon > 137.8347): 91 | return True 92 | if (lat < 0.8293 or lat > 55.8271): 93 | return True 94 | return False 95 | 96 | 97 | def gcj_to_wgs(gcjLon, gcjLat): 98 | if outOfChina(gcjLat, gcjLon): 99 | return (gcjLon, gcjLat) 100 | d = delta(gcjLat, gcjLon) 101 | return (gcjLon - d["lon"], gcjLat - d["lat"]) 102 | 103 | 104 | def wgs_to_gcj(wgsLon, wgsLat): 105 | if outOfChina(wgsLat, wgsLon): 106 | return wgsLon, wgsLat 107 | d = delta(wgsLat, wgsLon) 108 | return wgsLon + d["lon"], wgsLat + d["lat"] 109 | 110 | 111 | # -------------------------------------------------------------- 112 | 113 | # --------------------------------------------------------- 114 | # Get tile coordinates in Google Maps based on latitude and longitude of WGS-84 115 | def wgs_to_tile(j, w, z): 116 | ''' 117 | Get google-style tile cooridinate from geographical coordinate 118 | j : Longittude 119 | w : Latitude 120 | z : zoom 121 | ''' 122 | isnum = lambda x: isinstance(x, int) or isinstance(x, float) 123 | if not (isnum(j) and isnum(w)): 124 | raise TypeError("j and w must be int or float!") 125 | 126 | if not isinstance(z, int) or z < 0 or z > 22: 127 | raise TypeError("z must be int and between 0 to 22.") 128 | 129 | if j < 0: 130 | j = 180 + j 131 | else: 132 | j += 180 133 | j /= 360 # make j to (0,1) 134 | 135 | w = 85.0511287798 if w > 85.0511287798 else w 136 | w = -85.0511287798 if w < -85.0511287798 else w 137 | w = log(tan((90 + w) * pi / 360)) / (pi / 180) 138 | w /= 180 # make w to (-1,1) 139 | w = 1 - (w + 1) / 2 # make w to (0,1) and left top is 0-point 140 | 141 | num = 2 ** z 142 | x = floor(j * num) 143 | y = floor(w * num) 144 | return x, y 145 | 146 | 147 | def pixls_to_mercator(zb): 148 | # Get the web Mercator projection coordinates of the four corners of the area according to the four corner coordinates of the tile 149 | inx, iny = zb["LT"] # left top 150 | inx2, iny2 = zb["RB"] # right bottom 151 | length = 20037508.3427892 152 | sum = 2 ** zb["z"] 153 | LTx = inx / sum * length * 2 - length 154 | LTy = -(iny / sum * length * 2) + length 155 | 156 | RBx = (inx2 + 1) / sum * length * 2 - length 157 | RBy = -((iny2 + 1) / sum * length * 2) + length 158 | 159 | # LT=left top,RB=right buttom 160 | # Returns the projected coordinates of the four corners 161 | res = {'LT': (LTx, LTy), 'RB': (RBx, RBy), 162 | 'LB': (LTx, RBy), 'RT': (RBx, LTy)} 163 | return res 164 | 165 | 166 | def tile_to_pixls(zb): 167 | # Tile coordinates are converted to pixel coordinates of the four corners 168 | out = {} 169 | width = (zb["RT"][0] - zb["LT"][0] + 1) * 256 170 | height = (zb["LB"][1] - zb["LT"][1] + 1) * 256 171 | out["LT"] = (0, 0) 172 | out["RT"] = (width, 0) 173 | out["LB"] = (0, -height) 174 | out["RB"] = (width, -height) 175 | return out 176 | 177 | 178 | # ----------------------------------------------------------- 179 | 180 | # --------------------------------------------------------- 181 | class Downloader(Thread): 182 | # multiple threads downloader 183 | def __init__(self, index, count, urls, datas): 184 | # index represents the number of threads 185 | # count represents the total number of threads 186 | # urls represents the list of URLs nedd to be downloaded 187 | # datas represents the list of data need to be returned. 188 | super().__init__() 189 | self.urls = urls 190 | self.datas = datas 191 | self.index = index 192 | self.count = count 193 | 194 | def download(self, url): 195 | HEADERS = { 196 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.68'} 197 | header = ur.Request(url, headers=HEADERS) 198 | err = 0 199 | while (err < 3): 200 | try: 201 | data = ur.urlopen(header).read() 202 | except: 203 | err += 1 204 | else: 205 | return data 206 | raise Exception("Bad network link.") 207 | 208 | def run(self): 209 | for i, url in enumerate(self.urls): 210 | if i % self.count != self.index: 211 | continue 212 | self.datas[i] = self.download(url) 213 | 214 | 215 | # --------------------------------------------------------- 216 | 217 | # --------------------------------------------------------- 218 | def getExtent(x1, y1, x2, y2, z, source="amap"): 219 | pos1x, pos1y = wgs_to_tile(x1, y1, z) 220 | pos2x, pos2y = wgs_to_tile(x2, y2, z) 221 | Xframe = pixls_to_mercator( 222 | {"LT": (pos1x, pos1y), "RT": (pos2x, pos1y), "LB": (pos1x, pos2y), "RB": (pos2x, pos2y), "z": z}) 223 | for i in ["LT", "LB", "RT", "RB"]: 224 | Xframe[i] = mercator_to_wgs(*Xframe[i]) 225 | if source == "google_sat": 226 | pass 227 | elif source == "amap" or source == "amap_sat" or source == "google": 228 | for i in ["LT", "LB", "RT", "RB"]: 229 | Xframe[i] = gcj_to_wgs(*Xframe[i]) 230 | else: 231 | raise Exception("Invalid argument: source.") 232 | return Xframe 233 | 234 | 235 | def saveTiff(r, g, b, gt, filePath): 236 | fname_out = filePath 237 | driver = gdal.GetDriverByName('GTiff') 238 | # Create a 3-band dataset 239 | dset_output = driver.Create(fname_out, r.shape[1], r.shape[0], 3, gdal.GDT_Byte) 240 | dset_output.SetGeoTransform(gt) 241 | try: 242 | proj = osr.SpatialReference() 243 | proj.ImportFromEPSG(4326) 244 | dset_output.SetSpatialRef(proj) 245 | except: 246 | print("Error: Coordinate system setting failed") 247 | dset_output.GetRasterBand(1).WriteArray(r) 248 | dset_output.GetRasterBand(2).WriteArray(g) 249 | dset_output.GetRasterBand(3).WriteArray(b) 250 | dset_output.FlushCache() 251 | dset_output = None 252 | print("Image Saved") 253 | 254 | 255 | # --------------------------------------------------------- 256 | 257 | # --------------------------------------------------------- 258 | MAP_URLS = { 259 | "google": "https://mt1.google.com/vt/lyrs=r&x={x}&y={y}&z={z}", 260 | "google_sat": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", 261 | "amap": "https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}", 262 | "amap_sat": "https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}" 263 | } 264 | 265 | 266 | def get_url(source, x, y, z, style): # 267 | if source == 'amap': 268 | url = MAP_URLS["amap"].format(x=x, y=y, z=z) 269 | elif source == 'amap_sat': 270 | url = MAP_URLS["amap_sat"].format(x=x, y=y, z=z) 271 | elif source == 'google': 272 | url = MAP_URLS["google"].format(x=x, y=y, z=z) 273 | elif source == 'google_sat': 274 | url = MAP_URLS["google_sat"].format(x=x, y=y, z=z) 275 | else: 276 | raise Exception("Unknown Map Source ! ") 277 | return url 278 | 279 | 280 | def get_urls(x1, y1, x2, y2, z, source, style): 281 | pos1x, pos1y = wgs_to_tile(x1, y1, z) 282 | pos2x, pos2y = wgs_to_tile(x2, y2, z) 283 | lenx = pos2x - pos1x + 1 284 | leny = pos2y - pos1y + 1 285 | print("Total tiles number:{x} X {y}".format(x=lenx, y=leny)) 286 | urls = [get_url(source, i, j, z, style) for j in range(pos1y, pos1y + leny) for i in range(pos1x, pos1x + lenx)] 287 | return urls 288 | 289 | 290 | # --------------------------------------------------------- 291 | 292 | # --------------------------------------------------------- 293 | def merge_tiles(datas, x1, y1, x2, y2, z): 294 | pos1x, pos1y = wgs_to_tile(x1, y1, z) 295 | pos2x, pos2y = wgs_to_tile(x2, y2, z) 296 | lenx = pos2x - pos1x + 1 297 | leny = pos2y - pos1y + 1 298 | outpic = pil.new('RGBA', (lenx * 256, leny * 256)) 299 | for i, data in enumerate(datas): 300 | picio = io.BytesIO(data) 301 | small_pic = pil.open(picio) 302 | y, x = i // lenx, i % lenx 303 | outpic.paste(small_pic, (x * 256, y * 256)) 304 | print('Tiles merge completed') 305 | return outpic 306 | 307 | 308 | def download_tiles(urls, multi=10): 309 | url_len = len(urls) 310 | datas = [None] * url_len 311 | if multi < 1 or multi > 20 or not isinstance(multi, int): 312 | raise Exception("multi of Downloader shuold be int and between 1 to 20.") 313 | tasks = [Downloader(i, multi, urls, datas) for i in range(multi)] 314 | for i in tasks: 315 | i.start() 316 | for i in tasks: 317 | i.join() 318 | return datas 319 | 320 | 321 | # --------------------------------------------------------- 322 | 323 | # --------------------------------------------------------- 324 | def main(left, top, right, bottom, zoom, filePath, style='s', server="amap"): 325 | """ 326 | Download images based on spatial extent. 327 | 328 | East longitude is positive and west longitude is negative. 329 | North latitude is positive, south latitude is negative. 330 | 331 | Parameters 332 | ---------- 333 | left, top : left-top coordinate, for example (100.361,38.866) 334 | 335 | right, bottom : right-bottom coordinate 336 | 337 | z : zoom 338 | 339 | filePath : File path for storing results, TIFF format 340 | 341 | style : 342 | m for map; 343 | s for satellite; 344 | y for satellite with label; 345 | t for terrain; 346 | p for terrain with label; 347 | h for label; 348 | 349 | source : Google or amap 350 | """ 351 | # --------------------------------------------------------- 352 | # Get the urls of all tiles in the extent 353 | urls = get_urls(left, top, right, bottom, zoom, server, style) 354 | 355 | # Group URLs based on the number of CPU cores to achieve roughly equal amounts of tasks 356 | urls_group = [urls[i:i + math.ceil(len(urls) / multiprocessing.cpu_count())] for i in 357 | range(0, len(urls), math.ceil(len(urls) / multiprocessing.cpu_count()))] 358 | 359 | # Each set of URLs corresponds to a process for downloading tile maps 360 | print('Tiles downloading......') 361 | pool = multiprocessing.Pool(multiprocessing.cpu_count()) 362 | results = pool.map(download_tiles, urls_group) 363 | pool.close() 364 | pool.join() 365 | result = [x for j in results for x in j] 366 | print('Tiles download complete') 367 | 368 | # Combine downloaded tile maps into one map 369 | outpic = merge_tiles(result, left, top, right, bottom, zoom) 370 | outpic = outpic.convert('RGB') 371 | r, g, b = cv2.split(np.array(outpic)) 372 | 373 | # Get the spatial information of the four corners of the merged map and use it for outputting 374 | extent = getExtent(left, top, right, bottom, zoom, server) 375 | gt = (extent['LT'][0], (extent['RB'][0] - extent['LT'][0]) / r.shape[1], 0, extent['LT'][1], 0, 376 | (extent['RB'][1] - extent['LT'][1]) / r.shape[0]) 377 | saveTiff(r, g, b, gt, filePath) 378 | 379 | 380 | # --------------------------------------------------------- 381 | if __name__ == '__main__': 382 | start_time = time.time() 383 | 384 | #main(118.055917, 24.559724, 118.244753, 24.399450, 16, r'google_sat.tif', server="google_sat") 385 | main(float(sys.argv[1]), float(sys.argv[4]), float(sys.argv[2]), float(sys.argv[3]), int(sys.argv[5]), sys.argv[6], server=sys.argv[7]) 386 | 387 | end_time = time.time() 388 | print('lasted a total of {:.2f} seconds'.format(end_time - start_time)) 389 | --------------------------------------------------------------------------------