ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高德地图离线化部署实战:内网瓦片地图服务搭建全流程

高德地图离线化部署实战:内网瓦片地图服务搭建全流程 1. 离线化不是把地图App搬回家先想清楚整体架构先说结论这张图我前后折腾了快三周中间推倒重来了一次才把“在线高德”成功变成“内网高德”。如果你是刚接到类似需求第一件事别急着找下载工具先把架构想清楚否则后面返工成本特别高。内网环境下做高德地图离线化本质上要拆成两个独立的部分来看地图瓦片数据也就是你在网页上看到的那张“底图”。高德默认提供的是在线瓦片服务内网访问不了所以必须预先下载成图片文件部署到内网HTTP服务上。地图JS引擎也就是AMap这个JS对象负责在地图上画点、画线、弹窗、缩放、拖拽这些交互逻辑。这个引擎本身是JS文件可以下载到内网引用不需要联网但它默认会去请求高德的在线瓦片地址。所以关键在于把引擎默认的瓦片地址替换成你自己的内网服务地址。我见过不少人把这两件事混为一谈以为“把高德地图App装到内网服务器上”就行了完全不是一回事。网页端的地图本质是「瓦片底图 JS交互引擎」两层结构只有把这两层都离线化才能做到彻底不依赖外网。结合我接到的实际需求——在一台封闭内网服务器上搭一个监控大屏底图要求是德地图风格道路、小区、水域、绿地同时要支持点位标注、聚合展示、轨迹回放这些交互功能——我最终选型是这么定的组件在线方案离线方案说明底图瓦片高德在线瓦片服务预下载高德瓦片 → 内网Nginx静态服务保留高德风格的视觉效果交互引擎高德JS API 1.4.x或2.0本地引用AMapJS文件保留全部标注/聚合/轨迹API业务数据在线服务接口本地后端API点位、轨迹等业务数据自己管这个方案最大的好处是前端代码几乎不用改太多。原来怎么写AMap.Marker、AMap.Polyline离线方案里还是一样写只是瓦片地址变了。对于已经有在线版本的项目来说迁移成本最低。还有一个必须预判的坑坐标系。高德用的是GCJ-02火星坐标瓦片下载的时候一定要选对坐标系否则底图和业务点位会对不上。后面瓦片下载部分我会详细说这个问题。2. 瓦片下载从“在线预览”到“本地数据资产”2.1 下载工具选型我为什么选“全能地图下载器”“水经注”双组合市面上能下载高德瓦片的工具不少我实测下来比较靠谱的主流方案有三类全能地图下载器收费/破解版鱼龙混杂操作简单支持按行政区边界下载能导出多种瓦片格式适合批量处理大范围地图。水经注地图下载器老牌工具支持高德、谷歌、天地图等多源导出选项丰富但界面老旧高版本需要授权。开源脚本方案用Python写瓦片下载脚本通过http://webrd0{1-4}.is.autonavi.com/appmaptile?langzh_cnsize1scale1style8x{x}y{y}z{z}这类公开瓦片接口直接抓取灵活但需要自己处理并发、断点续传和坐标投影换算。我个人的建议是如果你是给客户交付用现成工具全能地图下载器或水经注为主省时间、容错高。我实际用的是全能地图下载器的绿色版按行政边界把整个市辖区下载完卫星图层和道路图层分开导出后面切图工具再合成。另外我写了一个Python脚本做断点续传的补充下载专门用来补漏。因为工具下载偶尔会有瓦片HTTP请求超时、漏瓦的情况脚本扫描本地目录和线上瓦片做比对补拉。2.2 下载前的关键参数配置坐标系、层级范围、图层选择无论用什么工具务必先确认这五个参数坐标系必须选GCJ-02火星坐标系。高德在线瓦片和官方JS API都是基于GCJ-02的。如果你下载的是WGS-84经纬度瓦片底图本身就会有一个固定的偏移后续点位全对不上还没法通过代码修正因为瓦片是矩阵切好的纠偏只能整体平移精度很难保证。瓦片层级Zoom Levels我按1~18级下载。1级是整张世界地图18级是街道级别对监控大屏场景来说已经足够。19级、20级文件体积成倍增加但内网用肉眼拼不出来明显差别一般不需要。下载范围用行政边界如“XX市XX区”下载会比矩形框选精确很多边缘不会出现大片空白水域或无关区域。图层拆分建议把「卫星影像」和「道路/标注」分开下载。高德在线地图在低层级1~10级用的是矢量渲染风格较高层会混合卫星图、道路图。如果你要叠加道路标注到卫星图上分开下载后切图阶段可以手动合成用CSS或者切图工具配置透明度。高德自己的叠加方案在离线后不一定能直接复用我后面踩了这个坑。瓦片格式工具默认导出大多是JPG少量透明图层是PNG。卫星底图用JPG没问题但道路标注层如果有透明背景需求必须导出PNG注意PNG文件体积大概比JPG大3倍磁盘成本要提前算。2.3 踩坑实录瓦片数量比你想象的大并发下载容易漏片我下载的是东南沿海某个地级市范围约6000平方公里1~18级全部层级下载完统计出来瓦片总量约为153万张。磁盘占用如下卫星图层JPG质量90约68GB道路标注图层PNG透明约21GB合计约89GB比预想的多出将近30%。导出的文件目录结构是这样的tiles/ ├── satellite/ │ ├── 01/000/001.jpg │ ├── 01/000/002.jpg │ └── ... └── road/ ├── 01/000/001.png └── ...高德瓦片采用的规则是{z}/{x}/{y}.jpg其中X是列Y是行原点在左上角web墨卡托投影约定。这个规则后面在Nginx发布和JS API对接时都要一致。下载过程中遇到的最典型问题是全能下载器默认线程数较高16线程下载到一半经常出现HTTP 403或请求超时导致部分瓦片缺失。我在完成第一轮下载后扫描了瓦片目录发现大约有0.3%的瓦片文件大小为0KB或缺失。这些随机缺失在在线预览时不易察觉一旦离线部署地图上就会出现“马赛克空洞”非常难看。所以我强烈建议任何工具下载完都必须做一次完整性校验。我的校验逻辑很简单遍历所有目录找出空白文件0KB。用线上瓦片地址逐个比对缺失文件https://webrd0{1-4}.is.autonavi.com/appmaptile?langzh_cnsize1scale1style8x{x}y{y}z{z}。对缺失和空洞瓦片做单线程补下载不要用多线程否则又会超时。3. 离线瓦片的高效部署从散落文件到稳定服务3.1 瓦片落盘后的目录规划与Nginx静态站点发布瓦片下载好后不是直接扔到服务器就能用的需要先规划目录结构。我的惯例是把底图数据和服务代码分开方便后续维护。推荐的目录结构/opt/maptiles/ ├── satellite/ # 卫星底图 │ ├── {z}/{x}/{y}.jpg ├── road/ # 道路标注层 │ ├── {z}/{x}/{y}.png └── mapserver/ # 前端页面资源JS、CSS、HTMLNginx配置如下用alias指令将URL路径映射到磁盘目录server { listen 8080; server_name localhost; # 卫星图 location /satellite/ { alias /opt/maptiles/satellite/; add_header Access-Control-Allow-Origin *; expires 7d; access_log off; autoindex off; } # 道路标图层 location /road/ { alias /opt/maptiles/road/; add_header Access-Control-Allow-Origin *; expires 7d; access_log off; autoindex off; } # 前端资源 location /map/ { alias /opt/maptiles/mapserver/; index index.html; } }关于alias和root这里有个很容易踩的坑root会把完整URI追加到路径后面而alias是直接替换匹配部分。对于/satellite/{z}/{x}/{y}.jpg这种路径两者写法不一样我用alias更直观。提示add_header Access-Control-Allow-Origin *;必须加。前端JS在请求瓦片图片时虽然是标准img标签多数情况下不会CORS拦截但如果你后续要做Canvas像素级处理、或者从线上瓦片URL加载到Canvas再重绘跨域限制就会冒出来。提前加上省得后面排查。3.2 为什么不建议直接用Python http.server发布生产瓦片很多人图省事在内网直接用python -m http.server 8080发布瓦片目录。实测下来有两个明显问题性能差Python内置服务器是单线程的瓦片请求量大时地图拖动、缩放会瞬间请求几十张瓦片排队明显拖动卡顿。不支持Range和HEAD优化浏览器和地图引擎对瓦片请求可能发起多个并发Nginx可以配合open_file_cache优化而http.server完全没有。所以我的建议是生产环境一律用Nginx或者其他高性能静态服务器Caddy、OpenResty都可以。开发调试阶段用http.server临时顶一下没问题正式交付前一定换掉。如果内网服务器连Nginx都没装比如刚初始化好的CentOS安装也很简单yum install -y nginx systemctl enable nginx systemctl start nginx记得把防火墙放行对应端口这个看似基础的操作实际交付时我见过有人卡了一下午原因是内网防火墙默认DROP了8080端口。4. 高德JS API离线接入把在线地图无缝替换成内网瓦片4.1 引入高德JS API的正确方式本地化JS文件而不是外链高德JS API官方提供的是外链方式一般是这样script typetext/javascript window._AMapSecurityConfig { securityJsCode: 你的安全密钥, } /script script srchttps://webapi.amap.com/maps?v2.0key你的keypluginAMap.Scale,AMap.ToolBar/script内网环境直接外链会加载失败所以需要把maps?v2.0对应的JS文件下载到本地。但高德的JS文件经过动态加载内部还会请求其他资源文件比如https://webapi.amap.com/maps/amap.js、https://webapi.amap.com/maps/plugins/...等。离线化时不能只下载一个JS文件要完整镜像webapi.amap.com下JS API依赖的相关资源。最简单的方法是在能联网的机器上用wget镜像整个webapi.amap.com/maps目录然后传到内网。wget -r -np -nH --cut-dirs1 -P amap-offline https://webapi.amap.com/maps/镜像完成后把本地引用改为script typetext/javascript window._AMapSecurityConfig { securityJsCode: 你的密钥, } /script script src/map/amap-offline/maps?v2.0keylocal_keypluginAMap.Scale,AMap.ToolBar/script这里有个小坑高德JS API在初始化的时候会去验证key和securityJsCode的合法性如果域名白名单内配置的是在线域名内网访问时可能出现INVALID_USER_SCODE错误。我当时的解决办法是在高德开放平台申请一个专用的key把域名白名单配置成服务器的内网IP或内网域名。如果是纯离线部署完全无法访问外网高德JS API实际上还是会定位或请求安全验证接口这种情况下有两种处理方式要么在开发阶段就验证好key要么考虑用开源Leaflet替换引擎。据我实测高德JS API 1.4.x对离线环境相对友好2.0的securityJsCode验证更严格所以离线项目我推荐使用1.4.x版本能减少很多兼容问题。4.2 自定义瓦片图层的核心代码TileLayer与getTileUrl魔法现在进入整篇博文最核心的部分。高德JS API支持通过AMap.TileLayer自定义瓦片图层我们只需要重写getTileUrl让它从内网Nginx取瓦片就能实现离线底图渲染。以1.4.x版本为例核心代码如下// 创建地图 var map new AMap.Map(container, { zoom: 12, center: [120.1536, 30.2875], // 以杭州某坐标为例 mapStyle: amap://styles/whitesmoke, // 可换样式但离线不一定支持 }); // 自定义离线瓦片图层 var offlineLayer new AMap.TileLayer({ getTileUrl: function(x, y, z) { // 高德瓦片坐标转成标准XYZ // 注意如果工具导出的Y轴方向和JS API不完全一样这里要处理翻转 return http://10.10.0.8:8080/satellite/ z / x / y .jpg; }, zooms: [1, 18], visible: true, opacity: 1, }); // 添加到地图 map.add(offlineLayer);细心的朋友会发现这里没有对X/Y做坐标变换。原因是高德在线瓦片和下载工具导出的瓦片绝大多数都遵循同一个约定左上角为原点X向右递增Y向下递增XYZ规范。用全能地图下载器和水经注导出的规范瓦片和高德JS API的请求坐标是一致的不需要额外反转。但如果你用的是某些TMS规范Y轴向上的瓦片目录就要做y Math.pow(2, z) - 1 - y的翻转否则地图会上下颠倒。这里还有个容易被忽略的细节AMap.TileLayer的tileSize默认是256高德在线瓦片也是256。如果你的瓦片源是512尺寸需要额外设置tileSize: 512否则渲染会模糊且对齐错误。4.3 为什么卫星图和道路图要分层叠加透明度与渲染顺序我们下载了卫星和道路两个图层。在离线地图中最好的模拟在线地图“混合地图”效果的方式是叠两层TileLayer卫星图在下面道路标注在上面并给道路图层设置透明度。var satelliteLayer new AMap.TileLayer({ getTileUrl: function(x, y, z) { return http://10.10.0.8:8080/satellite/ z / x / y .jpg; }, zooms: [3, 18], }); var roadLayer new AMap.TileLayer({ getTileUrl: function(x, y, z) { // z层级大于等于13时才有单独道路标注图下面会细说 if (z 13) return http://10.10.0.8:8080/blank.png; return http://10.10.0.8:8080/road/ z / x / y .png; }, opacity: 0.8, zooms: [3, 18], }); map.add([satelliteLayer, roadLayer]);这里有我踩过的一个大坑低层级的道路标注图不存在。高德在线地图在1~12级时地图是矢量渲染的没有独立的道路PNG切片或者说这些切片的样式非常简陋。我最初下载了1~18级道路层结果发现1~12级下面某些区域的PNG文件是空白或者缺失的。解决办法是下载时选择“矢量路网混合导出”或者“自定义样式导出”但这类高级功能很多工具默认不带。我的备选方案低层级1~12级直接用高德的「矢量底图样式」导出也就是把在线渲染好的底图整体导出成瓦片包含道路、河流、绿地然后高层级13~18级用卫星图道路标注叠加。这样最接近高德在线效果。但这也意味着瓦片下载任务量翻倍。如果只是给领导演示大屏低层级直接用道路瓦片也能看不必纠结。4.4 关于高德JS API 1.4.x和2.0的离线选型对比这里单独说一嘴因为我已经被问了好几次。做离线化建议用1.4.x而非2.0。两张表看懂差异对比项JS API 1.4.xJS API 2.0加载方式传统script标签支持按需加载ES Module离线兼容性较好安全校验相对宽松更严格离线部署容易报code错误自定义TileLayer支持写法直观支持但部分插件需要额外引用Loca数据可视化需要额外引入Loca插件2.0支持Loca 2.0但离线化更复杂维护状态官方仍在维护主流推荐我选用的是1.4.x后面讲的交互功能都以1.4.x为准。如果你非要用2.0做离线需要提前把AMapLoader的依赖也离线化镜像更麻烦而且安全密钥校验也是个麻烦事。5. 交互功能的内网实现点位、聚合、轨迹一个都不能少底图出来以后很多人的第一反应是“是不是太简单了”。其实地图底图只是画布真正的价值在交互功能。离线环境下高德JS API保留了大量交互API但凡是依赖在线服务的功能POI搜索、逆地理编码、路线规划都不能用了因为那些要请求高德在线服务接口。下面是我在离线大屏里落地的几类交互功能以及对应的替代思路。5.1 海量点位标注用简化版Marker而非AGM监控大屏常见的需求是在地图上撒几百上千个设备点位点击弹窗显示详情。直接使用AMap.Marker逐个add数量到500以上时地图会明显掉帧拖动卡顿。这是DOM节点太多造成的。我的做法参考高德的PointSimplifier思路但不引入额外插件直接分批渲染Marker并配合CSS样式在数据量500以下完全够用不需要引入额外插件。实现方式var markerList []; for (var i 0; i points.length; i) { var point points[i]; var marker new AMap.Marker({ position: [point.longitude, point.latitude], content: div classdevice-marker device- point.status /div, offset: new AMap.Pixel(-10, -10), title: point.name, }); marker.on(click, function(e) { // 弹窗逻辑 infoWindow.open(map, e.target.getPosition()); infoWindow.setContent(buildDetailHtml(e.target)); }); markerList.push(marker); } map.add(markerList);CSS里定义状态样式.device-marker { width: 20px; height: 20px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); cursor: pointer; } .device-normal { background-color: #33cc66; } .device-warning { background-color: #ffcc00; } .device-error { background-color: #ff3333; }5.2 聚合功能组件能用但要处理离线数据边界点多了就必须聚合。高德JS API 1.4.x有官方聚合插件AMap.MarkerClusterer离线时同样可以使用。核心代码如下map.plugin([AMap.MarkerClusterer], function() { var clusterer new AMap.MarkerClusterer(map, markerList, { gridSize: 60, // 聚合网格像素大小 minClusterSize: 5, // 最少聚合个数 maxZoom: 16, // 最大聚合层级 zoomOnClick: true }); });但这里要注意聚合组件在离线环境下正常工作的前提是你必须把amap-clusterer.js这个插件JS一并本地化。我第一次只离线了主JS文件没下载插件JS结果页面报AMap.MarkerClusterer is not a constructor。后来把https://webapi.amap.com/maps/plugins/AMap.MarkerClusterer.js补拉下来引用就好了。另外高德的聚合样式默认是一个蓝绿色气泡如果想自定义成红色、橙色、绿色分别表示不同严重程度需要重写renderClusterIcon方法这个不算复杂但注意一定不要重新创建地图实例否则性能会崩。5.3 轨迹回放与路径展示Polylone和矢量动画轨迹回放是大屏项目比较炫的一块。离线环境下不影响轨迹展示本身因为路径数据是业务自己的不依赖在线导航API。我用了AMap.Polyline逐步更新Marker位置的方式实现var line new AMap.Polyline({ path: pathData, // 轨迹坐标数组 strokeColor: #00b3ff, strokeWeight: 4, strokeOpacity: 0.8, lineJoin: round, lineCap: round, }); map.add(line); map.setFitView(line); // 轨迹播放动画 var carMarker new AMap.Marker({ position: pathData[0], content: div classcar-icon/div }); map.add(carMarker); var step 0; var timer setInterval(function() { step; if (step pathData.length) { clearInterval(timer); return; } carMarker.setPosition(pathData[step]); // 可以计算方向做图标旋转 }, 100);这里有个提升体验的细节要基于两点坐标计算方位角让小车图标朝向路线方向不然看起来在倒着开。计算方式用简单的经纬度夹角function getAngle(start, end) { var radLat1 start.lat * Math.PI / 180; var radLat2 end.lat * Math.PI / 180; var lngDiff (end.lng - start.lng) * Math.PI / 180; var angle Math.atan2( Math.sin(lngDiff) * Math.cos(radLat2), Math.cos(radLat1) * Math.sin(radLat2) - Math.sin(radLat1) * Math.cos(radLat2) * Math.cos(lngDiff) ); return angle * 180 / Math.PI; }5.4 POI搜索、逆地理编码这类“依赖在线服务”的功能怎么替代这是离线化改造的中心痛点。我在大屏上需要一个“点击地图弹出地址描述”的功能在线版直接用AMap.Geocoder就行但离线后这个方法直接失效因为Geocoder要请求高德在线服务。我的替代方案是在Nginx里做一个反向代理把高德在线地理编码接口的内网代理地址映射出来。前提是内网能访问外网才能走通如果内网完全不可上网就只能用离线思路下载全国POI数据集网上有很多开放数据集比如OSM、阿里云DataV地理数据用PostGIS存到本地。用ST_ReverseGeocode自己写一个逆地理编码接口。离线大屏上直接调用这个本地接口。不过现实里大量内网环境其实做了“单向访问限制”——内网可以访问外网的部分接口外网不能访问内网。这种情况下反向代理很脆弱所以我最终的方案是在业务后端挂一份区域坐标点表手动配置好常用地点的经纬度和名称做一个简化的本地检索接口。确实省事、也足够稳定。6. 内网部署的最后一公里环境适配与高频踩坑修复到这里你的离线地图基本成型了。但真正部署到内网环境时还有几个坑提前知道可以省一天调试时间。6.1 内网Linux环境升级Python版本带来的连锁问题有个热搜词提到“内网linux x86环境升级python版本”这个容易和地图瓦片下载脚本扯上关系。如果你在服务器上跑Python瓦片下载或切片脚本系统自带的Python版本往往不够新尤其是CentOS 7自带的2.7而高德瓦片下载脚本经常依赖requests、concurrent.futures这些新特性。升级Python时有几个注意点不要动系统默认的/usr/bin/python软链接。CentOS的yum依赖Python 2.7直接替换会造成yum崩溃。用源码编译安装到/usr/local/python3独立目录再通过/usr/local/python3/bin/python3调用。安装依赖前先配好内网pip源不能访问外网时离线安装wheel包特别痛苦。我通常提前下载好requests、pyproj、Pillow等包的wheel刻盘带到内网。wget https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz tar -zxvf Python-3.9.18.tgz cd Python-3.9.18 ./configure --prefix/usr/local/python3 --enable-optimizations make make install ln -s /usr/local/python3/bin/python3 /usr/bin/python36.2 域名白名单导致的地图加载失败高德JS API在线版本会校验Referer但离线部署后你的页面URL不再是高德后台配置的域名可能是IP、内网域名、甚至localhost。此时会出现地图空白、控制台报INVALID_USER_SCODE的错误。解决办法有两个方向用内网域名部署并把这个域名加入高德开放平台的白名单。适合内网有DNS服务器的情况但很多隔离内网不具备。用高德JS API 1.4.x的兼容模式。1.4.x支持不配置securityJsCode也能正常工作只是控制台会打一行提示。所以在离线项目里我最后选择了1.4.x并且不加载在线密钥校验纯本地点位渲染完全没问题。6.3 关于连接HTTPS与XHR类型的暗坑如果你内网页面本身是用HTTPS协议部署的而瓦片服务是HTTP浏览器会判定为“混合内容”直接拦截瓦片请求。这是自查时最容易忽略的。两种解法给内网Nginx也配上自签SSL证书瓦片URL用https://10.10.0.8:8080/...浏览器会提示证书不信任需要内网所有终端安装根证书。最简单粗暴的整个内网访问都用HTTP因为本来就是内网项目安全边界已经由网络隔离承担了没必要非上HTTPS找麻烦。6.4 瓦片加载慢或卡顿的排查思路离线地图最怕的是瓦片加载卡顿。如果确认Nginx配置没问题、网络也没问题那大概率是瓦片请求没有命中缓存。我之前的Nginx配置中expires 7d是为了让浏览器缓存瓦片但有些地图引擎会给瓦片URL加随机参数导致缓存失效。排查方法打开浏览器F12查看瓦片请求URL是否带有?_随机数这类参数。如果有去掉getTileUrl返回URL后面的随机参数如果没有带检查Nginx访问日志是不是每个瓦片都重复请求了。用open_file_cache优化打开文件描述符能显著提升Linux下大量小文件的访问性能。open_file_cache max200000 inactive20s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;6.5 常见地图白屏、瓦片黑块的快速排查清单这里整理一个排查清单给所有离线地图部署者备用现象可能原因处理方法整图白屏JS API文件未正确本地化或引用路径错误检查浏览器Network里JS加载是否200个别瓦片黑块瓦片文件缺失或0KB补齐对应{z}/{x}/{y}文件底图偏暗两个TileLayer叠加时设置了不透明度调整opacity或去掉叠加瓦片错位/上下颠倒瓦片坐标系TMS/XYZ不一致在getTileUrl做Y轴翻转标注点漂移坐标叠加了未经纠偏的数据确保数据是GCJ-02或单独纠偏转换地图操作卡顿瓦片请求量大或DOM Marker过多启用聚合、优化Marker数量、Nginx开缓存最后再分享一个实用小技巧如果你时间紧张又必须快速交付一个看起来像样的内网地图大屏我的建议是先用低层级1~12级的全国底图跑通Demo再针对业务区域下载高层级瓦片。这样一次性下载量可以从89GB降到8GB左右而且Demo验收足够。等确认架构没问题、领导点头了再逐步补全精细层级。我见过太多人一开始就想把全国18级全下下来结果下载了三天项目还没动工。离线地图本身不是新东西但把高德JS API、自定义瓦片图层、业务交互这三个层面同时打通还是有不少门道。希望这篇实战记录能让你少走几步弯路。有具体问题欢迎交流。
返回列表