简介面向需要离线地图能力的C#桌面开发爱好者这套基于GMap.Net库深度二次开发的项目以WPF构建界面实现了多图源在线浏览与瓦片缓存保存。它在保留谷歌地图、必应地图、OpenStreetMap等主流图源的同时额外加入高德地图图源并支持自动或手动将浏览过的瓦片存储到本地形成离线数据集在网络不稳或节省流量场景下仍能顺畅查看地图。包内共565个文件包括217个C#源文件、93个DLL库、45个XML配置、54个PNG图像资源及XAML界面文件、PDB调试信息、Cache缓存文件等压缩包约48.8MB兼顾源码阅读与运行引用。已有541人学习浏览。透过完整的WPF前端与GMap.NET扩展代码可观察缓存瓦片的下载策略、离线访问接口、多图源切换逻辑和高德适配方式适合希望研究离线地图原理或快速搭建同类工具的开发者参考。1. GmapDownloader.zip一个把在线地图搬运成本地瓦片库的地图下载器做过GIS或者大屏可视化的人多半经历过这种场景项目交付前客户要求现场网络断开离线演示还要带底图。打开在线地图白屏截图缩放模糊得没法看。GmapDownloader.zip就是为解决这个需求而打包的一类命令行地图下载工具读入一个经纬度范围按瓦片金字塔规则把在线地图逐级取回本地最终落成一个/z/x/y.png的标准瓦片目录可以被常见离线地图框架直接读取。文章面向有GIS或Web开发基础的从业者帮你把这套工具的选型逻辑、命令参数、坐标偏移坑、文件校验方法一次讲清楚。不画饼只讲怎么下载、怎么拼、怎么验收。2. 瓦片金字塔与下载器原理为什么选GmapDownloader而不是网页截图2.1 瓦片金字塔一张世界地图其实是几万张256像素小图在线地图服务商不会把一整张全球地图发给浏览器。地图按z/x/y三级编号划分为正方形小图每张边长256像素这就是瓦片。层级 z 从0开始z0 时全世界只有1张瓦片每放大一级行列数翻倍瓦片总数变成上一级的4倍。z18 时全世界的理论瓦片数超过 680 亿张当然实际只需要下载你关心的那一小块区域。本地离线地图复用的是同一套坐标系统。瓦片目录z/x/y.png里的 y 轴向下原点在左上角不同的地图服务对 y 方向的定义有差异目前主流接口大多遵循 XYZ 规范也就是 x 向右、y 向下递增。而较早的 TMS 规范 y 方向相反下载器里通常会提供--tms这样的开关来切换坐标换算。搞错这个方向拼出来的大图会上下颠倒。GmapDownloader 这类工具做的工作说白了就是把你给出的 bbox 经纬度范围换算成每一级 z 下的瓦片行列号范围再逐个发请求取回文件。2.2 选型判断下载器与网页截图、浏览器插件的本质差异有人问我的区域不大用浏览器截图不行吗实际做过一次就知道在线地图是动态加载的拖拽缩放时地图服务只加载当前视口内的瓦片截屏次数再多拿到的也只是屏幕分辨率下的位图不是瓦片库。浏览器插件能批量抓图但控制不了并发、没有断点续传请求频繁还会把页面卡死。GmapDownloader.zip 这类工具的优势在于它把范围计算、请求调度、文件落地做成了解析参数就可以重复执行的命令行流程默认只做一件事——按规则把瓦片抓下来适合几平方公里到整座城市的批量任务。另一个常被忽略的选型点在于输出结构。网页截图输出的是图片后续无法继续做投影转换、矢量叠加、切片发布瓦片库输出的是标准目录可以直接被 Leaflet、MapLibre GL 这类前端框架离线加载也可以用 GDAL 拼成 GeoTIFF 交给 GIS 桌面软件用。同样的下载行为瓦片库是半成品截图是废品。这也是我推荐你接受“命令行工具包”这种形式的原因。2.3 从经纬度到瓦片号下载器内部做的核心换算要判断一个下载器靠不靠谱先看它的坐标换算逻辑。标准 Web Mercator 投影下给定经纬度(lat, lng)和层级 z瓦片行列号的计算公式如下import math def wgs84_to_tile(lat: float, lng: float, z: int): lat_rad math.radians(lat) n 2 ** z x int((lng 180.0) / 360.0 * n) y int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) return z, x, y这里n是当前层级下全球的边长瓦片数asinh(tan(lat_rad))是 Web Mercator 的纵轴映射比老式写法更稳定不会在接近两极时出现精度丢失。很多老脚本直接用log(tan 1/cos)计算在高纬度区域会有浮点误差。拿到角落点的瓦片号后再按瓦片大小循环下载即可。换个角度理解这套换算你给出的 bbox 会被分成两对瓦片号x从左上角到右下角递增y同理。下载器的日志里如果打印了tile 5 / 1682指的就是当前层级下的第5张、共1682张。我在实际使用中会先跑一遍--dry-run之类的参数让工具只打印瓦片数不实际下载确认数量级再开全量。避免一开始就把流量跑空。3. 用GmapDownloader.zip跑通首个下载任务命令、参数与首批数据3.1 解压与运行环境准备先确认三件事拿到GmapDownloader.zip之后先解压看包内结构。常见的压缩包里会有一个可执行文件、一个config.example和一份README。解压命令如下unzip GmapDownloader.zip -d ./gmap cd ./gmap ls -la解压后先确认三件事有没有主程序文件是 Python 脚本、Node 脚本还是编译好的二进制配置文件的字段有没有填包内有没有自带的requirements.txt或package.json按需安装依赖。大部分同类工具要么依赖 Python 3 的requests要么依赖 Node 的axios先把这些依赖装齐。Windows 环境建议在 Windows Terminal 里跑命令别双击运行。下载任务常常跑几小时终端窗口一关进程就没了。我更习惯在 Linux 服务器或者 WSL 里跑这类任务用nohup或tmux挂后台日志落盘随时回头查看进度。3.2 最小下载命令一个街区范围的核心参数一个最小的下载命令长这样./gmapdl \ --url https://mt1.example.com/vt/lyrsm221hlzh-CNz{z}x{x}y{y} \ --bbox 28.20,112.90,28.30,113.10 \ --min-zoom 10 --max-zoom 17 \ --threads 8 --retry 3 --timeout 15 \ --out ./tiles这个命令里每个参数都有实际意图。--url是瓦片服务的 URL 模板{z}/{x}/{y}是占位符下载器会逐个替换--bbox按min_lat,min_lng,max_lat,max_lng顺序给出下载范围--min-zoom和--max-zoom决定下载哪些层级--threads是并发线程数--retry是失败重试次数--timeout是单张瓦片的超时时间。第一次跑建议把并发调到2确认单张能下载再逐步加大。关于包含输出目录的--out ./tiles不同工具写法略有差异有的用--dir有的用--output。跑之前先扫一眼 README 确认参数名。这套最小命令跑完目录里会出现类似这样的结构tiles/ z10/ x123/ y456.png y457.png metadata.jsonmetadata.json是下载器自己生成的元数据文件记录 bbox、zoom 范围、URL 模板和下载时间。别删它后续增量更新和瓦片校验都靠这个文件。3.3 多层级批量下载瓦片量级与磁盘占用的估算方法大范围下载前先估算体量避免硬盘被写爆。瓦片数量随层级呈四倍增长z16 时全球瓦片数为 2^16 × 2^16 ≈ 43 亿你的业务范围可能只涉及其中几百张。估算用这个经验公式每张瓦片在 Web Mercator 投影下的地面宽度约为40075 * cos(lat) / 2^z公里。以纬度 28.4° 为例z17 时单张瓦片约 0.27 公里宽0.1° × 0.1° 的范围约 11 公里见方z17 约需要 41 × 41 ≈ 1681 张瓦片。这还覆盖了从 z10 到 z17 所有层级实际总数会高不少因为低层级一张瓦片就盖住整个范围。内存占用参考普通道路底图的 PNG 瓦片单张 30–150 KB卫星影像经常到 300–500 KB。拿刚才的范围乘一下全层级拉下来的体积从几十 MB 到几个 GB 不等。我通常在命令后补一句du -sh ./tilesz18 比 z17 的瓦片数翻四倍如果范围里有建筑级影像需求先只下最大层级用--min-zoom 17 --max-zoom 18单独跑一遍基础层级留给全量任务能省一半流量。4. GmapDownloader.zip避坑排查指南坐标偏移、并发限流与断点续传4.1 瓦片和GPS轨迹对不上坐标偏移的排查思路现象下载出来的瓦片能拼成图但叠加现场采集的 GPS 轨迹或控制点坐标后整体偏移了几十到几百米且偏移方向接近固定。原因部分在线瓦片服务输出的坐标基准不是 WGS-84而是做了位置加密的坐标系统。下载器本身不负责纠偏它只是按你传入的范围去请求瓦片如果瓦片服务端输出的规则和下载器默认的坐标系不一致结果自然对不上。解决先确认你用的瓦片服务的坐标系描述。用单张瓦片文件测找一个路口中心点对比下载影像和 GPS 坐标的差值如果差值稳定可以在下载后再给整张 GeoTIFF 做一次平移或重投影。另一个更省事的方案是把 bbox 当作偏移量反向补偿。比如实测偏移约 300 米就先按西移 0.003°、北移 0.003° 重新计算下载范围让影像落到期望区域。跑批之前先用一个点验证偏移关系再全量下载。4.2 下载到一半被限流HTTP 429与403现象日志里开始出现429或403下载速度肉眼可见地下降甚至归零重试也不生效。原因绝大多数瓦片服务都有请求频率限制。线程数拉到 16 以上单一 IP 的短时间请求量会触顶服务端返回 429 表示请求过速403 表示 User-Agent 被识别成异常客户端。解决把并发调下来。我常用的安全区间是 4–8 线程每张瓦片之间保持约 50ms 的间隔同时给请求带上真实的浏览器 User-Agent。很多下载器提供了这两个参数./gmapdl \ --threads 4 \ --retry 3 \ --retry-delay 500 \ --user-agent Mozilla/5.0 (Windows NT 10.0; Win64; x64)--retry-delay的单位一般毫秒遇到 429 后等待时间翻倍重试不要一上来就循环快重反而容易封得更死。还有一点容易被忽略限流通常按 IP 结算公司出口有一个共享 IP别人跑高并发任务也会牵连你。遇到这种情况就只能错开高峰时段。4.3 中断后重跑全量断点续传的三种处理方式现象下载任务跑到一半断电、断网或者不小心关了终端重跑同一命令时它从第一张又开始下载已经下过的瓦片被覆盖既浪费时间又容易被限流。原因当前这批命令行工具默认没有维护“已下载列表”它只认范围不认已完成文件。解决分三种情况。一是工具自带--resume参数那么直接加参数重跑下载器会逐文件检查存在性二是工具不提供续传参数那就保留原始输出目录不删除重跑时先看日志里跳过的数量是否大于 0三是都没有时用文件系统层面做补救find ./tiles -name *.png -size 0 -delete这条命令用来清理下载中断产生的 0 字节文件避免它们占用空间并污染后续拼接。大多数下载器在请求失败时会先创建空文件再写内容断点时这些空文件会成为“存在”的假象。重跑全量会把这些空文件覆盖但如果你打算用跳过存在文件的策略续传必须先把它们清掉。4.4 拼接时出现空洞和黑线瓦片边界取整问题现象下载完把瓦片拼成大图发现边缘有细黑线放大后看到一条缺失的像素列拼出来的图整体位置偏了半张瓦片。原因下载器在把 bbox 转瓦片号时边界取整方式错了。正确做法是左上角向下取整、右下角向上取整保证范围完全覆盖如果都是四舍五入就会在边角漏一列或一行瓦片。解决检查下载器日志里输出的瓦片范围手动验证一下边界。拿经纬度坐标换算后确认坐标位于左上角瓦片范围内。GmapDownloader 这类工具如果有--padding或--margin参数可以设置 0 到 1 张瓦片的余量。实际项目里我会在 bbox 四周主动外扩一点例如--bbox 28.195,112.895,28.305,113.105扩大约 0.005° 到 0.01°换来边界瓦片的完整。代价只是多下载几十张瓦片但能省去拼接阶段的暗坑。4.5 下载完成却全是白片瓦片是坏的现象下载日志显示全部完成但打开影像后大片区域是白色或花屏文件大小只有几百字节。原因限流或者原图缺失时服务端会返回一张默认的空白瓦片或错误提示图下载器正常保存了文件却没有校验内容。解决离线检查所有瓦片的文件大小把明显异常的挑出来。我常用这两条命令配合find ./tiles -name *.png -type f -size -1k -print | head -20 find ./tiles -name *.png -type f -size 800k -print | head -20第一条找出低于 1KB 的可疑文件第二条找出远超正常体积的异常文件。很多下载器不支持细粒度内容校验文件大小成了最容易获取的体检指标。巡检完再对可疑瓦片用命令重下比如curl -o ./tiles/z/17/x/123/y/456.png \ https://mt1.example.com/vt/z17x123y456单张补漏比全量重跑便宜得多。5. 瓦片库落地GDAL拼接大图与离线预览5.1 本地离线预览让瓦片目录直接跑起来下载目录本身就是标准瓦片库只要一个静态文件服务就能预览。最简单的办法是进入瓦片父目录用 Python 起一个静态服务cd ./tiles python3 -m http.server 8080然后在浏览器里用离线网页加载。前端代码里指向本地瓦片路径!doctype html html head link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script /head body div idmap stylewidth: 100%; height: 600px/div script const map L.map(map).setView([28.25, 113.0], 15); L.tileLayer(http://localhost:8080/{z}/{x}/{y}.png, { maxZoom: 17 }).addTo(map); /script /body /html这里的L.tileLayer路径占位符需要和之前下载目录层级完全匹配。如果你的下载器输出的目录是多层嵌套的例如z10/x123/y456.png要把 URL 里的{z}/{x}/{y}.png改成对应结构或者下载时指定扁平目录模式。Leaflet 的瓦片格式默认是/z/x/y.png所以下载器输出目录能不能被直接读取是选型时的一个硬指标。5.2 把瓦片拼成一张GeoTIFFGDAL方式与像素方式离线网页预览适合演示但要做空间分析、导入 CAD 或者发到其他 GIS 系统最好把瓦片库拼成一张带坐标的大图。常见做法分为两步先用网格坐标把瓦片合成一张大 PNG再用 GDAL 写入地理参考。下面的 Python 脚本用 Pillow 按行列顺序拼接from PIL import Image import os TILE_SIZE 256 def merge_tiles(src_dir: str, z: int, out_path: str): x_dirs [d for d in os.listdir(src_dir) if d.isdigit()] x_dirs.sort(keyint) max_y 0 tiles {} for x in x_dirs: y_files os.listdir(os.path.join(src_dir, x)) y_files [f for f in y_files if f.endswith(.png)] y_files.sort(keylambda f: int(f.split(.)[0])) for y in y_files: tiles[(int(x), int(y.split(.)[0]))] (x, y) max_y max(max_y, int(y.split(.)[0])) x_min int(x_dirs[0]); x_max int(x_dirs[-1]) y_min 0; y_max max_y width (x_max - x_min 1) * TILE_SIZE height (y_max - y_min 1) * TILE_SIZE canvas Image.new(RGB, (width, height)) for (x_idx, y_idx), (x_name, y_name) in tiles.items(): tile_img Image.open(os.path.join(src_dir, x_name, y_name)) canvas.paste(tile_img, ((x_idx - x_min) * TILE_SIZE, (y_idx - y_min) * TILE_SIZE)) canvas.save(out_path)脚本的思路是先扫描所有 x 目录再对每个 x 下的 y 文件排序算出合成画布尺寸最后逐个贴图。注意若 max_y 从 0 开始算高度是(max_y 1) * TILE_SIZE如果你下载的 y 编号不是从 0 开始要手动记录最小 y 值否则画布底部会多出一片空白。拼出 PNG 后用 GDAL 写投影信息gdal_translate -of GTiff \ -a_srs EPSG:3857 \ -a_ullr 左上角X 左上角Y 右下角X 右下角Y \ merged.png merged.tif-a_ullr后面跟上 Web Mercator 坐标系下的左上角和右下角坐标数值可以通过单张瓦片的角点换算得到不想手工算就用工具输出瓦片范围再按 256 倍像素算出地理范围。GDAL 写入后建议再做一次gdaladdo merged.tif 2 4 8 16 32生成影像金字塔后续加载大图会流畅很多。这只是简单场景下的方案。如果下载器支持输出成带地理参考的 GeTiff 或 Mbtiles直接用输出产物会省去这一步。没有的话上述方式是一套验证过无依赖的方案。5.3 坐标参考系与重投影EPSG:3857还是EPSG:4326在线地图瓦片默认使用 Web Mercator 投影也就是 EPSG:3857单位是米。如果你最终只需要底图显示ESPG:3857 就可以了。要把影像叠加到 GPS 坐标或 CAD 坐标上可能需要转换到 EPSG:4326 或其他平面坐标系。用 GDAL 一行命令完成重投影gdalwarp -t_srs EPSG:4326 -r bilinear merged.tif merged_wgs84.tif-r bilinear指定重采样方法缩放影像时双线性插值比默认算法平滑适合影像底图。做这一步前先确认坐标系参数如果原数据坐标系标注错了warp 出来的坐标偏差可能比不转换还大。下载器 metadata.json 里记录了 bbox 和坐标格式拼图前后交叉比对能最大程度避免这个尴尬。6. 进阶增量更新、瓦片校验与现场演示的一线经验6.1 增量更新别每次都全量重下地图数据会不定期更新尤其卫星影像旧版本和新版本差异明显。GmapDownloader 这类工具不一定自带增量能力但 metadata.json 给了实现空间。下载完成后保留上次的瓦片目录再次执行时只对指定 zoom 层级做差异处理把新旧 metadata 的下载时间对比时间不一致的瓦片用find -newer筛出来重下方向更精准。这个手段没有标准命令可照抄但思路是这样。6.2 交付前校验瓦片数量与理论值对比每次下载完先看瓦片总数是否等于理论值。统计命令find ./tiles -name *.png | wc -l find ./tiles -name *.png -size -1k | wc -l第一条确认真实瓦片数第二条确认异常小文件数量。把前者除以理论值如果低于 99%说明有多处漏下载后者如果超过 0说明有白片隐患。完成这一步再把瓦片目录拷进演示机现场基本不会翻车。总结成一句话下载器不是跑完命令就结束它的产出质量必须经过文件级校验。6.3 一个留下深刻印象的教训早年做园区离线演示我用了某款瓦片下载器下载日志显示 100% 完成。演示当天现场大屏上的卫星图在某个位置出现连续的白斑凑近看是几块空白瓦片。我在线下一张张测过但忽略了 0 KB 文件。后来重新做了一套流程下载完先删 0 字节文件再对一元以下的小文件单独重试最后做全量数量校验。这个顺序后来救了我很多次。你自己上手时建议把这四步写成一个固定脚本再交付比信任任何工具日志都可靠。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?