简介:面向安卓开发者的高德地图标注与路线规划学习资源,包含完整示例工程与可运行安装包,适合需要快速上手地图服务、实现兴趣点标注和驾车步行路径规划的初中级开发者。压缩包共一百个文件,约三点三四兆,以Java源码、编译类文件、界面布局、图标和依赖库为主,另含数据库与配置数据,整体呈现标准安卓项目形态,便于直接导入工程分析。已有三百零五人学习下载,资源覆盖高德地图定位、标注、路线绘制及实时路况处理等核心流程,并涉及开发者账号创建、地图实例初始化、标记物添加、路径规划接口调用等关键概念。通过阅读源码可掌握自定义标记图标、路径线条绘制、距离计算及主线程界面更新等关键写法,配合安装包可对照运行效果,是地图功能开发的高性价比参考样例。
1. 这个 zip 里装的不是地图,是一套可复用的高德地图“三件套”工程
如果你手里拿到一个名为“高德地图标注,路线规划_地图定位.zip”的压缩包,先别急着双击解压,里面大概率是一个以高德地图 JS API 为核心的地图示例工程,覆盖三个高频需求:在地图上做业务标注、按起终点规划路线、获取并显示当前位置。做地图页面最耗时间的不是调 API,而是把这三个模块串起来,还要处理坐标系、权限和加载顺序问题。这套代码的价值是把最常用的地图操作拆成可复制的模板,适合前端开发、GIS 初学者,也适合内部系统需要快速搭一个地图演示的场景。我按这个方向给你拆一份能跟着做的落地笔记,如果你自己搭过几个地图页,会发现下面这些坑你可能都眼熟。
2. 先把地基打好:高德地图 JS API 的 Key 配置、瓦片加载和坐标系
2.1 选型理由:为什么用高德而不是 Leaflet 或百度地图
在真正动手前,先想一个问题:为什么项目里常见做法是选高德,而不是 Leaflet 加 OpenStreetMap,也不是百度地图?高德地图 JS API 的优点在于它已经把国内常用的底图瓦片、POI 搜索、路线规划、地理编码都打包成了同一套接口,前端只需要关心业务数据。Leaflet 是轻量,但底图源在国内访问速度和稳定性都很难控,而且瓦片坐标系通常是 WGS84,和高德地图像素坐标对不上,得自己做投影换算。百度地图也有完整生态,但它的坐标是 BD-09,从 GPS 或第三方数据转过来又多一道算法。高德用 GCJ-02 国测局坐标,和国内大多数设备采集的坐标天然兼容。
从团队协作角度看,高德 JS API 的文档、社区示例和维护节奏都更有优势,遇到问题在开发者社区翻一翻能搜到很多现成案例。这个 zip 工程里如果标注、路线规划、定位三个模块都用了同一个地图实例,那选高德就非常自然,因为它的插件体系允许把驾车、步行、骑行、定位等模块按需加载,不需要像 Leaflet 那样到处拼插件。下表是我常用的选型对比,你可以直接抄到方案文档里:
| 地图方案 | 坐标系 | 底图国内加载 | POI/路线能力 | 集成成本 |
|---|---|---|---|---|
| 高德 JS API | GCJ-02 | 快 | 齐全,同一体系 | 低 |
| Leaflet + OSM | WGS84 | 慢,受网络影响 | 弱,需自接服务 | 中 |
| 百度 JS API | BD-09 | 快 | 齐全,但二次转换 | 中 |
2.2 最小可运行页面:引入 JS API、设置 div、初始化 Map
先说结论:高德 JS API 2.0 的页面结构其实很固定,踩过一次坑之后基本就背下来了。最简版需要三个部分:一个要带 id 的 div 容器、一个引入脚本的<script>标签,以及一个初始化AMap.Map的实例。这里有一个容易忽略的点:2.0 版本要求配置安全密钥securityJsCode,否则控制台会报INVALID_USER_KEY。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>地图定位三件套</title> <style> #map { width: 100%; height: 600px; } </style> </head> <body> <div id="map"></div> <script> // 申请 Web 端 JS API 后获得的安全密钥,需要和 key 配对使用 window._AMapSecurityConfig = { securityJsCode: '你的安全密钥', }; </script> <script src="https://webapi.amap.com/maps?v=2.0&key=你的Key&plugin=AMap.Driving,AMap.Geolocation"></script> <script> const map = new AMap.Map('map', { zoom: 12, // 初始缩放级别,城市级用 12 比较合适 center: [116.397428, 39.90923], // GCJ-02 坐标,建议从这里开始 viewMode: '2D', }); </script> </body> </html>这段代码的逻辑很直白:在页面加载时先设置window._AMapSecurityConfig,然后通过 script 标签把地图库拉下来,plugin参数里提前声明后面要用的路线规划和定位插件。初始化AMap.Map时,center必须是一个[经度, 纬度]的数组,顺序别写反。如果写成[39.90923, 116.397428],地图会直接定位到海里,这是最常见的翻车原因。zoom的合法范围一般是 2 到 20,之前有同事写成 1,底图直接变成一片灰。
2.3 瓦片加载异常与离线场景:把官方瓦片源换成自己的代理
如果只是正常在线使用,高德瓦片不需要你关心。但内网部署或网络质量不稳定的环境里,底图加载慢甚至无法加载就成了致命问题。高德地图瓦片本质是标准的 XYZ 切片地址,通过域名加x/y/z参数拼出来。常见做法是在 Nginx 里把官方瓦片地址反代一层,页面内部仍然用官方的AMap.Map,但把默认瓦片源替换成你自己的代理地址。
替换方式有两种:一种是直接改AMap.Map的tileLayer参数,传入一个自定义AMap.TileLayer,在里面指定getTileUrl返回代理地址;另一种更省事,用内网代理改写请求路径,前端不用动代码。第一种方式我更推荐,因为代理地址里可以加本地缓存参数,流量可控。注意代理层需要支持跨域 CORS,否则瓦片会响应回来但 Canvas 画不上去,控制台报Failed to load错误。如果你在内网离线环境,可以把常用层级瓦片预下载到对象存储,用同样的代理地址映射到本地目录。
2.4 先弄清 GCJ-02 和 WGS84:标注定位不漂移的前提
高德地图所有坐标都遵循 GCJ-02,也就是俗称的国测局坐标系。它和 GPS 设备直接输出的 WGS84 坐标有几十到几百米的偏移,这在后面的定位和标注里会反复折磨你。处理思路很简单:如果是设备采集的 GPS 坐标,存库时先转成 GCJ-02 再交给地图;如果是从其他平台导出的坐标,导出前就要确认坐标系。高德 JS API 提供了一个现成的转换函数AMap.convertFrom,把 WGS84 坐标传进去可以转出一组 GCJ-02。不要自己去网上抄一个偏移算法,各地区的偏移量不是常数,抄来的公式大概率在某些城市翻车。
2.5 Key 的域名白名单和安全密钥的存放位置
高德 JS API 的 key 是发到浏览器端的,只要页面部署出去,key 就无法真正保密。所以高德控制台允许给 key 配置域名白名单,只有白名单里的域名才能正常发起请求。本地调试时,把localhost和127.0.0.1加进白名单,否则你在本地打开会看到一片空白。安全密钥securityJsCode也尽量不要写在公共前端代码里,内部项目无所谓,对外项目建议由服务端下发,页面加载时拿到再动态设置window._AMapSecurityConfig。哪怕被看到,域名白名单也限制了它不能在别的站点盗用。
3. 地图标注:从点标记到自定义内容,把“标注”做成真正能用的数据层
3.1 标注的本质:AMap.Marker 覆盖物与数据结构设计
“标注”在地图 SDK 里叫覆盖物(Overlay),点标注对应AMap.Marker。常见误区是一上来先写怎么画 Marker,而忽略了数据模型设计。实际做标注前,我会先定义一条数据的字段:name、lng、lat、desc、type、status。坐标字段拆成lng和lat而不是合一个position,是为了后续存后端数据库或做空间检索方便。标注数据的结构和纯前端代码无关,但决定了你能不能把标注直接导出成 GPX、KMZ 或 GeoJSON。
这套思路和很多“数据标注”场景是相通的,只是地图标注关心的是地理坐标,不是像素框。如果你接触过遥感图像标注,回头理解地图标注会很快,区别只在于坐标系和底图。这里我把标注数据设计成一个数组,每个元素对应一个点:
const poiList = [ { id: 1, name: '北门', lng: 116.397428, lat: 39.90923, desc: '临时停车点,限时 30 分钟', type: 'parking', }, { id: 2, name: '服务中心', lng: 116.398, lat: 39.908, desc: '可办理租借手续', type: 'service', }, ];3.2 添加多个标注点:循环添加 Marker 与信息窗体
有了数据数组,往地图上加标注就是一次循环。这里不要每个 Marker 单独写一段初始化代码,数据多了会把人写崩溃,而且后续改样式要改几十处。标准做法是先建一个空数组保存所有 Marker,然后遍历poiList来实例化。
const markers = []; poiList.forEach((item) => { const marker = new AMap.Marker({ position: [item.lng, item.lat], title: item.name, // 可以自定义 icon,比如按 type 映射不同颜色 icon: item.type === 'parking' ? 'https://webapi.amap.com/theme/v1.3/markers/n/mark_red.png' : 'https://webapi.amap.com/theme/v1.3/markers/n/mark_blue.png', }); // 点击标注点弹出信息窗体,内容用模板字符串拼 marker.on('click', () => { const info = new AMap.InfoWindow({ content: `<div style="padding:6px;"><b>${item.name}</b><br><span style="font-size:12px;color:#666;">${item.desc}</span></div>`, offset: new AMap.Pixel(0, -30), }); info.open(map, marker.getPosition()); }); map.add(marker); markers.push(marker); });AMap.Marker的position只接受[lng, lat]数组,这是高德 2.0 的 API 设计,传对象可以初始化但后续拿getPosition()会多绕一步。offset用来调整信息窗体与标注点之间的位移,默认窗体会盖住标注点。点击事件里info.open(map, marker.getPosition())的第二个参数必须是AMap.LngLat实例或数组,直接把item.lng传进去会报错。
3.3 把标注数据抽成 JSON:解决“写死坐标”的维护难题
很多新手的前端代码里,标注坐标直接写在 JS 里,功能能跑,但换一批数据就要改代码。更常见的做法是把标注数据抽成一个独立points.json文件,前端通过fetch加载。但如果你用file://协议直接双击打开 HTML,fetch会因跨域被浏览器拦截,页面能开、标注不出来。解决方式有两种:本地起一个http-server,或者把数据放在同一个 JS 文件里用window.__DATA__暴露。
// points.json { "version": "20240501", "points": [ { "name": "北门", "lng": 116.397428, "lat": 39.90923 } ] } // 在 main.js 里加载 fetch('./points.json') .then(res => res.json()) .then(data => { data.points.forEach(addMarker); }) .catch(err => console.error('标注数据加载失败', err));我喜欢在项目里保留version字段。地图数据更新频繁,这个字段用来核对线上资源有没有缓存住,排查“我改了数据页面还是老样子”的问题非常有用。加载失败时不要只console.log,要看 Network 面板里是 404 还是 CORS,后面有一章专门讲排查。
3.4 标注性能与样式:海量点聚合与 label 避让
标注点超过 500 个,逐个渲染 Marker 会出现明显的卡顿,缩放时尤其严重。高德 JS API 提供了AMap.MarkerCluster插件,把空间上相近的点合并成一个聚合簇,缩放时动态拆分。我一般的做法是先把所有 Marker 实例建出来,再交给聚合器,而不是让聚合器自己重造数据。
// 在 plugin 参数里加上 AMap.MarkerCluster const cluster = new AMap.MarkerCluster(map, markers, { gridSize: 60, // 聚合网格大小,默认 60,点密集时可以调小 maxZoom: 15, // 大于该层级不再聚合,直接显示单点 });关于label避让,这是另一个低级坑。如果给每个 Marker 都加一个label,文字会在小缩放级别时互相重叠,看起来一团糟。高德底层有碰撞避让机制,但前提是你在AMap.LabelMarker或Marker的label配置里设置了zIndex和direction。不要指望地图自动把每个注入的 label 都排得漂亮,最稳妥的方案是聚合模式下只显示聚合数量,展开后再显示 label。
4. 路线规划:驾车、步行、骑行规划的实现与“轨迹修正”细节
4.1 路线规划 API 选择:驾车、步行、骑行与 AMap.Driving 等类
高德 JS API 的路线规划不是内置功能,而是以插件形式提供,常见的有AMap.Driving(驾车)、AMap.Walking(步行)、AMap.Riding(骑行)、AMap.Transfer(公交)。首次加载地图时,script 标签的plugin参数里就需要把这些类名带上,否则运行时new AMap.Driving会直接报AMap is not defined或者Driving is not a constructor。这个报错很有误导性,问题不在AMap全局对象,而在插件没加载。
选型时注意:驾车支持途经点,步行一般不支持,骑行是否支持以官方文档为准。如果你的页面场景是“从当前定位点出发,到某个标注点”,我会优先选驾车和骑行,因为它们返回的路径坐标可以直接用于轨迹绘制。下表是三个常用类的返回结果差异:
| 插件类 | 返回结果结构 | 典型用途 |
|---|---|---|
| AMap.Driving | routes[].steps[].path | 车辆调度、导航演示 |
| AMap.Walking | routes[].steps[].path,路径较细 | 园区步行指引 |
| AMap.Riding | routes[].steps[].path | 骑行路线推荐 |
4.2 起终点参数与途经点:从标注点直接发起路线规划
路线规划最常见的使用方式是点击地图上的标注点,把它作为终点,再拿当前定位作为起点。这里有个细节:search方法的起终点参数可以传数组坐标、也可以传AMap.LngLat实例、还可以传地址字符串。但我建议统一传数组坐标,因为地址字符串会触发地理编码请求,返回结果多一层回调,出错了很难判断到底哪一步失败。
const driving = new AMap.Driving({ policy: AMap.DrivingPolicy.LEAST_TIME, // 最少时间,还有避开高速等策略 // 高性能模式,按需开启 }); const start = [116.397428, 39.90923]; const end = [markers[0].getPosition().lng, markers[0].getPosition().lat]; const waypoints = [ [116.398, 39.908], // 途经点,数量限制以当前官方文档为准 ]; driving.search(start, end, { waypoints }, (status, result) => { if (status === 'complete' && result.routes && result.routes.length) { renderRoute(result.routes[0]); } else { console.error('路线规划失败', result); } });代码里的waypoints是可选参数,不传也可以。但如果你传了,注意高德对途经点数量有限制,点太多会返回INFO_UNKNOWN_ERROR,我踩过 20 个途经点直接报错的坑,后来砍到合理数量才稳定。policy参数也重要,物流场景选LEAST_TIME不如选LEAST_FEE,你要根据业务判断。
4.3 路线结果绘制与步行段处理:用插件还是自绘?
AMap.Driving.search成功回调之后,插件其实默认会把路线画到地图上,控制台 Network 里能看到完整的 route 数据。但默认绘制样式是蓝色细线,特点不明显。常见做法是拿到result.routes[0]后,自己提取路径坐标,用AMap.Polyline重新画一条高亮路线。这样做的好处是路线数据可以复用,比如你要把整条轨迹存库或做历史回放。
function renderRoute(route) { const path = []; route.steps.forEach((step) => { // 每个 step 是一个路段对象,它的 path 是经纬度坐标数组 step.path.forEach((p) => { path.push([p.getLng(), p.getLat()]); }); }); const polyline = new AMap.Polyline({ path, strokeColor: '#1c78ff', strokeWeight: 6, lineJoin: 'round', }); map.add(polyline); map.setFitView([polyline]); // 让路线完整显示 }注意route.steps[].path里的元素是AMap.LngLat实例,不是原始数组,所以上面用了p.getLng()和p.getLat()。新手最容易在这里翻车:直接push(p)到 Polyline 的path里,结果线路画不出来。setFitView保证整条路线都入视野,不然规划完发现路线在屏幕外,用户体验很差。步行段处理同理,公交换乘里的步行连接段坐标可以抽出来和主线合并,也可以单独用虚线画,业务表达更清楚。
4.4 路线规划的回调异常与限流:err 信息怎么读
高德路线规划的错误信息很“朴实”,永远是一个result对象,里面有info字段。常见的INVALID_USER_KEY是 key 配错,SERVICE_NOT_EXIST是插件类没加载,DAILY_QUERY_OVER_LIMIT是配额用完。读错误时不要只看status === 'error',要把result.info打出来,很多问题藏在字面上。
真正麻烦的是后端高并发请求触发限流。JS API 的路线规划在浏览器端调用有限流,前端页面同时发起 10 个search,很可能中间几个返回一个未知错误。我一般的做法是给路线请求加一个简单的失败重试,首次失败后延迟 500ms 再试一次,最多试三次。这不算破坏接口规范,只是把偶发限流消化掉。重试时还要注意上一次绘制的路线没有清理,要在每次规划前把旧的 Polyline 移除,避免叠加。
5. 避坑清单:标注、路线规划、定位三处最容易翻车的地方
5.1 定位不准:浏览器提示位置不可用
现象:代码里调用navigator.geolocation.getCurrentPosition,浏览器弹窗拒绝授权,或者直接返回Position unavailable。原因:高版本浏览器要求定位页面必须是 HTTPS 协议,非加密页面默认禁用精确定位。解决:本地开发时用localhost访问,部署时保证 HTTPS;如果一定要在 http 内网环境使用,就得换高德的 IP 定位作为兜底。高德插件AMap.Geolocation能同时处理 HTTPS 和 IP 定位两种模式,优先级由参数控制。
const geolocation = new AMap.Geolocation({ enableHighAccuracy: true, // 尝试用 GPS 等高精度源 timeout: 10000, // 10 秒没结果就算失败 zoomToAccuracy: false, // 不要在定位成功后自动缩放到精度范围 }); geolocation.getCurrentPosition((status, result) => { if (status === 'complete') { const { position } = result; console.log('定位坐标', position); } else { console.warn('定位失败,走 IP 兜底', result); } });enableHighAccuracy在手机浏览器上会让设备搜索 GPS 信号,耗电更快,但精度能到十几米。PC 上一般就用 WiFi 定位,设置成true也不会明显变慢。zoomToAccuracy这个参数很多人忽略,默认会让人瞬间拉近到楼块级别,体验很突兀,我一般会关掉。
5.2 标注点位置漂移到“海对岸”
现象:用手机采集了一个坐标,填入标注后地图上跑到大洋彼岸。原因:设备 GPS 输出的是 WGS84 坐标,高德地图只接受 GCJ-02,两个坐标系不经过转换直接叠加,就会产生几百米甚至跨区域的偏移。解决:入库前用AMap.convertFrom做一次转换。
const gpsCoords = [116.47, 39.84]; // WGS84 坐标 AMap.convertFrom(gpsCoords, 'gps', (status, result) => { if (status === 'complete' && result.locations) { const gcj = result.locations[0]; addMarker([gcj.lng, gcj.lat]); } });注意convertFrom是一次网络请求,不要在地图complete事件里循环调用几千次,那样会直接把并发打满。稳妥做法是前端批量转换后缓存结果,或者在服务端一次性处理。
5.3 路线规划不显示或只显示一条直线
现象:search返回complete,但地图上没有路线,偶尔只有一条连接起终点的直线。原因:直线是你自己画的起点和终点连线;路线没显示是因为没有把result.routes[0].steps里的坐标正确取出来,或者把AMap.LngLat实例直接当数组用了。解决:按 4.3 节的renderRoute函数重取坐标;先console.log(route.steps[0].path)确认结构再画。
5.4 地图瓦片空白,控制台报安全密钥错误
现象:地图容器渲染出来了,但底图大面积空白,只有标注点和路线。原因:高德 2.0 要求window._AMapSecurityConfig必须在引入 JS API 之前定义,而且安全密钥要和 key 配对。很多教程里代码顺序错了,密钥配置写在了 script 加载之后。解决:把window._AMapSecurityConfig移到引用webapi.amap.com的 script 标签之前,然后清理浏览器缓存再看。
5.5 本地 file:// 方式打开页面,定位和 fetch 同时失效
现象:双击 HTML 文件,标注和路线都正常,但定位失败,或者 JSON 数据加载不出来。原因:file://协议下浏览器把当前页面当成不安全的源,Geolocation API 直接不可用,同时fetch本地 JSON 会因为 CORS 拦截。解决:在项目目录下开一个本地静态服务,访问http://localhost:8080来调试。这个场景特别坑,因为你把项目放到服务器上时又好了,会让人误以为是定位代码的问题。
6. 验证与进阶:把标注数据导出为 GPX、接瓦片缓存与后续扩展
先把验证方法说掉。页面启动后,打开浏览器开发者工具的 Network 面板,过滤api或restapi,能看到地图瓦片、路线规划请求、地理编码请求。如果瓦片请求一堆 403 或 CORS,不用怀疑,就是 Key 和服务域名的问题。定位成功时,Network 里会有location相关的请求,响应里能看到经纬度和精度半径。
验证通过后,一个很实用的进阶是把标注数据导出成 GPX 文件,这样可以在高德地图车机版、奥维互动地图等工具里导入。GPX 本质是 XML,我给你一个可复用的生成函数:
function exportGpx(poiList) { const header = '<?xml version="1.0" encoding="UTF-8"?><gpx version="1.1" creator="map-app" xmlns="http://www.topografix.com/GPX/1/1">'; const footer = '</gpx>'; const wpts = poiList.map(p => ` <wpt lat="${p.lat}" lon="${p.lng}"> <name>${p.name}</name> <desc>${p.desc || ''}</desc> </wpt>`).join(''); const blob = new Blob([header + wpts + footer], { type: 'application/gpx+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'markers.gpx'; a.click(); URL.revokeObjectURL(url); }GPX 的lat和lon写在属性里,顺序别搞反,否则导入到其他工具会全部落在西半球。导出时高德标注数据的坐标是 GCJ-02,GPX 工具如果是国际标准读取,会再次出现偏移。所以导出前最好先转成 WGS84,或者明确知道目标工具的坐标系。这里就牵出更大的话题:地图数据链路里,坐标系转换永远避不开。
再往后可以做瓦片缓存。高德地图瓦片是永久有效的静态资源,如果内网用户多,用 Nginx 做一层正向代理缓存,能显著降低出口流量和加载时间。也可以预下载常用城市 5-17 级的瓦片放本地静态目录,离线环境一样能显示底图。我见过有人把整套高德瓦片同步到 Linux 服务器里,再在页面上把瓦片源地址指向本地,效果和在线完全一致,只是更新需要定期同步。
落到习惯上,我自己的经验是:先跑通最小集,再谈添加功能。不要一上来就同时搞标注、路线、定位加聚合。先把一个干净的Map实例跑起来,确认瓦片和 Key 没问题,再初始一个 Marker,再走一次路线规划,最后才叠加定位。每一步都用 Network 面板看一次请求,出问题能立刻定位。这个习惯帮我避开了无数个“最后一起出 bug 都不知从哪查”的深夜。希望帮到你。
本文还有配套的精品资源,点击获取