☰
高德地图JS API圆形与矩形覆盖物创建编辑实战指南
2026/10/9 12:48:07 网站建设 项目流程

做地图类后台功能时,最高频的需求往往不是花哨的轨迹回放,而是让用户在地图上"圈个范围"。我这边接过一个门店运营系统的需求:运营人员要在高德地图上画一个圆形或矩形的区域,用来表示门店配送范围、服务半径、电子围栏,而且画完还必须能随时调整,因为业务范围经常变动。

这套需求落到技术实现上,就是高德地图JS API里的圆形和矩形创建与编辑。当时我在高德地图Web端JS API 2.0上把这套逻辑完整实现了一遍,从最基础的创建覆盖物、开启编辑模式,到编辑后数据回存、多图形切换管理,前前后后踩了不少实用的坑。这篇文章把完整做法和坑位记录梳理一遍,给正准备实现类似地图绘制功能的同学做个参考。

1. 为什么用AMap.Circle和AMap.Rectangle,而不是一块Polygon打天下

1.1 三者能力对比:从数据结构到交互形态

高德JS API里的圆形和矩形是两个独立的覆盖物类,并不是Polygon的语法糖。AMap.Circle的核心数据是一个圆心坐标加一个以米为单位的半径;AMap.Rectangle的核心数据是一个经纬度边界Bounds,也就是西南角和东北角两个点。而AMap.Polygon需要维护一组顶点坐标,每一个点都是一个AMap.LngLat。

从存储结构看,圆和矩形的数据模型极其干净。后端存圆形时只需要{ center: [lng, lat], radius: 500 },存矩形时只需要{ sw: [lng, lat], ne: [lng, lat] },字段语义清晰,接口联调和数据库设计都省心。而用Polygon存储一个"看起来像圆"的图形,需要十几个甚至几十个顶点,回显时还要逐点重建,数据体积和复杂度都上去了。我做这个需求时特意对比过,不少人做"圆形"功能时习惯用Polygon逐点逼近,其实在高德生态里完全没必要,原生Circle类就是干这个的。

从编辑交互上看,Circle和Rectangle有现成的编辑器类——AMap.CircleEditor和AMap.RectangleEditor。打开编辑器后,覆盖物上会自动生成拖拽手柄,圆形有一个调整半径的手柄和圆心移动手柄,矩形有四个角点和四条边上的手柄,交互形态和设计稿基本一致。如果用Polygon去模拟圆,你虽然有AMap.PolygonEditor,但要把"调整半径"这个业务动作换算成"同时移动多个顶点",逻辑复杂不说,用户体验也生硬。

从渲染性能看,一个Circle对象内部就是一个圆图元,绘制开销很小。十几个顶点的Polygon在单个图形下没有明显区别,但当页面里出现几十个甚至上百个可编辑覆盖物时,顶点数量的差异就会体现到渲染帧率和内存占用上。

需求形态推荐方案核心原因
配送范围、服务半径、信号覆盖区AMap.Circle圆心+半径,数据简洁,编辑器自带
正矩形围栏、地图选块区域AMap.RectangleBounds两角点定位,编辑手柄完善
任意角度矩形、不规则多边形AMap.Polygon支持任意顶点,但编辑逻辑要自己处理

1.2 什么时候不应该用这两种专用类

Rectangle有一个硬限制:它只能画"边和经纬度平行"的正矩形,不能旋转,不能画平行四边形。如果你需要任意角度的矩形,比如地图上的倾斜车位线、楼栋外轮廓,就必须退回Polygon,自己保存4个顶点,并且接受PolygonEditor的编辑交互。

Circle虽然没有旋转问题,但业务上如果要求的是不规则区域,比如行政区划、小区边界、河道范围,那圆和矩形都不适用,老老实实用Polygon或GeoJSON数据加载。选型这件事,核心是先把业务场景说清楚,再决定覆盖物类型。我见过一个项目把几十个坐标点的多边形简化成矩形来存,结果业务验收时发现覆盖范围完全不对,返工成本很高。

2. 地图初始化和静态覆盖物的创建

2.1 加载JS API时把Editor插件一并声明

高德JS API 2.0的加载方式是在script标签的URL里带key和plugin参数。很多人在第一步就栽了跟头:只加载了基础地图库,没有声明Editor插件,等到代码里执行new AMap.CircleEditor()时直接报AMap.CircleEditor is not a constructor。

正确加载方式:

<script src="https://webapi.amap.com/maps?v=2.0&key=你的Key&plugin=AMap.CircleEditor,AMap.RectangleEditor"></script>

同时,在高德开放平台申请Key后,还需要配置安全密钥。如果你用的是JS API 2.0,必须在前置脚本里声明:

<script> window._AMapSecurityConfig = { securityJsCode: '你的安全密钥' } </script>

这里有个容易忽略的细节:开发调试阶段,到控制台把Key对应的域名绑定成localhost和127.0.0.1即可。如果不绑定,线上页面有时能加载地图,有时又会报Key无效,非常诡异。

2.2 地图实例化参数建议

const map = new AMap.Map('mapContainer', { zoom: 14, center: [116.397428, 39.90923], resizeEnable: true, viewMode: '2D' });

resizeEnable: true值得专门提一句。如果地图外层容器尺寸变化(比如侧边栏折叠、窗口缩放),没有这个参数时地图画面容易留白或变形。加了之后API会自动重算尺寸。

2.3 创建第一个静态圆形和矩形

创建圆形覆盖物:

const circle = new AMap.Circle({ center: new AMap.LngLat(116.397428, 39.90923), radius: 800, strokeColor: '#3366FF', strokeWeight: 2, strokeOpacity: 0.8, strokeStyle: 'solid', fillColor: '#3366FF', fillOpacity: 0.3, bubble: true, cursor: 'pointer', visible: true }); map.add(circle);

创建矩形覆盖物:

const bounds = new AMap.Bounds( new AMap.LngLat(116.34, 39.87), new AMap.LngLat(116.45, 39.95) ); const rectangle = new AMap.Rectangle({ bounds: bounds, strokeColor: '#FF9900', strokeWeight: 2, strokeOpacity: 0.8, fillColor: '#FF9900', fillOpacity: 0.3, bubble: true, cursor: 'pointer' }); map.add(rectangle);

这两个对象创建后是静态展示状态,用户可以看,但还不能拖拽和调整大小。真正的编辑能力在下一节。

2.4 几个显示参数的取舍经验

fillOpacity半透明度我习惯控制在0.2到0.4之间。太高会盖住底图要素,运营人员看不清区域内道路名和门牌号;太低又看不出选中感。strokeWeight推荐2到3,太细在高分屏上看不清,太粗会盖住图形的边界线。cursor: 'pointer'能在鼠标悬停时给出"可点击"的暗示,对后台系统的非技术用户很友好。

bubble: true表示覆盖物的鼠标事件可以冒泡到地图上,这样点击图形时地图不会误触发click事件。如果你的业务逻辑里需要在点击空白处时关闭编辑状态,这个参数记得保持true。

还有一个实际细节:AMap.Bounds的构造参数有两种写法,一种是四个数字(最小经度、最小纬度、最大经度、最大纬度),另一种是两个LngLat对象。后一种写法的可读性好很多,代码里一眼就能看出西南角和东北角。我全部统一用两个点的写法。

3. 可编辑模式的开启与交互控制

3.1 editable属性只是入口,Editor类才真正可控

在创建覆盖物时,Circle和Rectangle的构造参数里都支持editable属性,设为true后图形直接进入可编辑状态。这个属性适合快速验证功能,但不适合出现在正式业务里。原因很简单:编辑状态不可控,用户刚进入页面就看到一堆拖拽手柄,体验很糟糕。

我的做法是创建时一律editable保持默认false,需要编辑时动态创建Editor实例,通过open()和close()两个方法控制编辑状态的进入和退出。

Editor的用法:

let currentEditor = null; function enableEdit(overlay) { // 先关闭上一个编辑器,避免多个图形同时处于编辑状态 disableEdit(); if (overlay instanceof AMap.Circle) { currentEditor = new AMap.CircleEditor(map, overlay); } else if (overlay instanceof AMap.Rectangle) { currentEditor = new AMap.RectangleEditor(map, overlay); } if (currentEditor) { currentEditor.open(); } } function disableEdit() { if (currentEditor) { currentEditor.close(); currentEditor = null; } }

这段代码是整套编辑逻辑的核心骨架。instanceof判断覆盖物类型,分别创建对应的编辑器,同时确保全局只有一个编辑器在工作。有了这个基础,后续无论是工具栏按钮触发编辑、图形点击触发编辑,还是地图空白处点击关闭编辑,都只是在合适时机调用这两个方法。

3.2 编辑过程中的事件监听

编辑器实例和覆盖物对象都会抛出事件。AMap.CircleEditor上可以监听adjust事件,表示圆形的大小或位置正在被调整;AMap.RectangleEditor也有类似事件。

在实际项目中我发现,不同版本API的事件名并不完全一致,有些环境里鼠标拖拽过程中会高频触发adjust事件,有些环境里只有在松开鼠标那一刻才触发。如果业务逻辑依赖具体事件名,很容易在升级SDK版本后悄悄失效。

我这里提供一个不依赖具体事件名的兜底方案:不管用哪个事件做实时预览,都只在编辑器关闭时统一读取覆盖物的最终数据。也就是disableEdit()之后,再去getCenter()、getRadius()、getBounds(),拿到的数据永远是可靠的。

3.3 拖拽调整背后的数据变化原理

你可能会好奇,编辑手柄拖拽时,底层到底发生了什么。其实不管是CircleEditor还是RectangleEditor,本质都是在鼠标事件驱动下调用覆盖物的数据更新方法——圆编辑是反复调用circle.setCenter(lnglat)和circle.setRadius(radius),矩形编辑是反复调用rectangle.setBounds(bounds)。每调用一次,覆盖物对象就触发一次change事件。

理解这个原理之后,一个很自然的优化就出来了:如果你在change事件里写业务逻辑,比如实时计算面积、实时提交数据库,在鼠标拖拽过程中这个回调会被触发几十次,性能压力很大。正确姿势是拖拽过程中只更新界面上的预览值,松手后(编辑器close时)再做一次真正的状态提交。

4. 编辑结果的序列化、回显与局部更新

4.1 圆形和矩形分别要保存哪些数据

编辑完成后,从覆盖物对象上取值:

// 圆形数据 function getCircleData(circle) { const center = circle.getCenter(); // AMap.LngLat const radius = circle.getRadius(); // 单位:米 return { type: 'circle', center: [center.lng, center.lat], radius: radius }; } // 矩形数据 function getRectData(rectangle) { const bounds = rectangle.getBounds(); // AMap.Bounds const sw = bounds.getSouthWest(); // AMap.LngLat const ne = bounds.getNorthEast(); // AMap.LngLat return { type: 'rect', sw: [sw.lng, sw.lat], ne: [ne.lng, ne.lat] }; }

这里有两个单位细节需要注意。getRadius()返回的是米,不是像素。前端拿到以后传给后端,后端做空间计算(比如判断一个经纬度点是否在圆内)时,也统一用米做单位,不要混。bounds.getSouthWest()和getNorthEast()返回的是经纬度对象,我在序列化时把lng和lat拆成了数组,和后端接口的数据结构保持一致。

4.2 回显:创建一个新对象而不是恢复编辑态

从后端拿到保存的数据,重新生成覆盖物的过程,我称之为回显。核心逻辑很简单,根据type字段分别构造覆盖物:

function createOverlayFromData(data) { if (data.type === 'circle') { return new AMap.Circle({ center: new AMap.LngLat(data.center[0], data.center[1]), radius: data.radius, strokeColor: '#3366FF', strokeWeight: 2, fillColor: '#3366FF', fillOpacity: 0.3, bubble: true, cursor: 'pointer' }); } if (data.type === 'rect') { const bounds = new AMap.Bounds( new AMap.LngLat(data.sw[0], data.sw[1]), new AMap.LngLat(data.ne[0], data.ne[1]) ); return new AMap.Rectangle({ bounds: bounds, strokeColor: '#FF9900', strokeWeight: 2, fillColor: '#FF9900', fillOpacity: 0.3, bubble: true, cursor: 'pointer' }); } return null; }

一个容易犯的错是:回显时把editable: true一起带上了,结果页面打开所有图形都处于编辑状态,手柄到处乱飞。回显场景下只负责展示,编辑状态留到用户主动触发时再进入。

4.3 setCenter、setRadius、setBounds做局部更新

如果用户编辑完图形后,你不想重新创建覆盖物对象,可以用覆盖物自带的setter方法做局部更新:

circle.setCenter(new AMap.LngLat(lng, lat)); circle.setRadius(1200);
const newBounds = new AMap.Bounds( new AMap.LngLat(swLng, swLat), new AMap.LngLat(neLng, neLat) ); rectangle.setBounds(newBounds);

局部更新比"销毁重建"更高效的原因是:覆盖物的关联覆盖物层对象、事件绑定、以及编辑器实例都不需要重新初始化,尤其是编辑器实例还开着的时候,setBounds或setRadius更新数据不会导致编辑状态关闭。但这要求你始终持有原覆盖物对象的引用,所以我在项目里用了一个Map结构来管理多个图形的id和对象实例,后面会具体讲。

5. 实际项目里的坑位记录

5.1 插件漏声明,Editor变成undefined

这是最高频的报错,没有之一。症状是控制台报AMap.CircleEditor is not a constructor,但地图和覆盖物都正常显示。原因是script标签里的plugin参数没有带上AMap.CircleEditor和AMap.RectangleEditor。这个报错特别容易误判,因为基础地图API能正常用,只有编辑器相关的类缺失。排查时可以打开浏览器的Network面板,看初始加载的JS文件里是否包含Editor相关的文件。与其排查,不如一开始就规范写全plugin参数。

5.2 切换编辑对象时,旧编辑器没关闭

页面里有多个圆形和矩形时,如果用户点击了图形A进入编辑状态,又点击图形B想编辑B,此时A的编辑器没有close,会出现两个图形的编辑手柄同时存在,拖拽时数据互相干扰。

我的处理方式是维护一个全局currentEditor变量,在开启新编辑器之前先close掉旧的。同时配合一个currentOverlay变量记录当前被选中的覆盖物,在覆盖物的click事件里做切换。核心代码就是前面enableEdit函数里的那两行disableEdit()调用。

5.3 编辑事件监听放到了覆盖物上导致叠加

有段时间我把回调绑定在覆盖物的change事件上,每次编辑结束后都会触发更新。第二次进入编辑状态时,我没有解绑旧的回调,结果同一个事件被触发了两次,数据提交了两次。这类问题在覆盖物反复创建和销毁的场景里尤其隐蔽。

解决思路是绑定事件前先off解绑,或者用once一次性监听。如果是通过AMap.Circle构造参数里的events配置绑定的,那每次创建新实例就是全新的绑定,不会叠加;如果是用circle.on()动态绑定的,就要注意在不需要时circle.off()掉。

5.4 圆形半径的单位换算和精度

getRadius()返回的单位是米,高德底层是基于墨卡托投影换算到像素的。这里有个实际精度问题:在小范围内(几百米到几公里),圆半径的数值和后端用经纬度计算的球面距离误差很小,可以直接互用;但如果你的半径达到几十公里(比如省级信号覆盖),直接用平面换算会累积误差,后端做"点在圆内"判断时就可能出现边界上的点算不准。

处理办法是后端做空间判断时不要用简单的经纬度差勾股定理,而是用高精度球面距离算法,或者把数据落到支持空间索引的数据库里用地理函数算。前端这边不用过度操心,因为高德的显示层面已经做了墨卡托适配。

5.5 覆盖物层级zIndex和点击穿透

多个覆盖物叠在一起时,后添加到地图上的图形默认盖在先添加的图形上面。如果两个区域有重叠部分,点击时高德会触发最上层覆盖物的点击事件,下层覆盖物点不到。这会让用户以为"这个图形失联了"。

我在创建覆盖物时统一给zIndex赋值,并且遵循"正在编辑的图形最高"原则。激活某个图形时,动态把它设置为当前zIndex最大值加1,编辑器关闭后再恢复到常规值。这个细节很能提升体验,尤其在地图上有五六个业务区域交叠的密集场景里。

6. 落地扩展:让绘制结果真正进入业务

6.1 面积和周长的实时估算

业务侧经常会要求显示"当前圈选面积多大"。圆的面积可以直接算:

const area = Math.PI * radius * radius;

半径是米,面积是平方米。想换算成亩或者平方公里时,注意单位换算别写错:1平方公里等于1000000平方米,1亩约等于666.67平方米。

矩形的面积用边长相乘。矩形的边和经纬度平行,边长可以直接借助高德的距离计算工具:

const length1 = AMap.GeometryUtil.distance( new AMap.LngLat(sw.lng, sw.lat), new AMap.LngLat(ne.lng, sw.lat) ); const length2 = AMap.GeometryUtil.distance( new AMap.LngLat(sw.lng, sw.lat), new AMap.LngLat(sw.lng, ne.lat) ); const area = length1 * length2;

AMap.GeometryUtil.distance计算的是两个经纬度点之间的球面距离,比直接用坐标差平方和开根号准确得多,在日常业务精度范围内完全可用。把这个计算放到编辑器close之后的取值逻辑里,加上一个"面积:xx 平方米"的标签展示,运营侧就很满意了。

6.2 结合定位能力把初始图形放在用户当前位置

如果你的业务希望"进入页面自动在当前定位位置创建一个半径为1公里的圆形围栏",可以用高德的定位插件:

AMap.plugin('AMap.Geolocation', function() { const geolocation = new AMap.Geolocation({ enableHighAccuracy: true, timeout: 10000 }); geolocation.getCurrentPosition(function(status, result) { if (status === 'complete' && result && result.position) { const center = [result.position.lng, result.position.lat]; const circle = new AMap.Circle({ center: center, radius: 1000 }); map.add(circle); map.setCenter(center); } else { // 定位失败时回退到默认中心点 } }); });

这里有个体验细节:定位是异步的,创建圆形之前地图已经初始化好了,所以定位成功后要手动map.setCenter()把视野移动过去,否则用户看到的是一个在屏幕边缘的圆形,容易以为功能坏了。

6.3 多图形管理与列表仪表盘联动

页面上有多个圆形矩形时,仅仅在地图上点选还不够直观。我当时的做法是左侧加了一个"区域列表",每一行对应一个覆盖物,展示类型图标、中心坐标、半径或边界信息。点击列表某一行,地图自动缩放到该图形范围,同时高亮选中状态。

const overlayMap = new Map(); // id -> overlay实例 let idCounter = 0; function addOverlay(data) { const overlay = createOverlayFromData(data); const id = 'overlay_' + (++idCounter); overlayMap.set(id, overlay); map.add(overlay); return id; } function zoomToOverlay(id) { const overlay = overlayMap.get(id); if (!overlay) return; if (overlay instanceof AMap.Circle) { map.setZoomAndCenter(15, overlay.getCenter()); } else if (overlay instanceof AMap.Rectangle) { map.setBounds(overlay.getBounds()); } }

用Map结构管理覆盖物实例,比散落在一堆变量里不知道强多少倍。删除图形、回显初始化、批量操作,都可以基于这个Map做得干净利落。右键菜单删除也很好接,在覆盖物上监听rightclick,弹出自定义菜单,确认后调用map.remove(overlay)并从Map里移除。

6.4 再往前走一步的想法

圆形和矩形的创建编辑做完之后,如果再遇到"不规则区域"需求,可以考虑把数据升级为Polygon,并把编辑入口统一抽象成一个绘制工具条:圆形、矩形、多边形、编辑、删除,五个按钮一辆面包车。绘制这块,高德还有AMap.MouseTool可以辅助做鼠标绘制,但可编辑效果仍然要回到各自的Editor上。

以我做完这个功能的整体体会,地图覆盖物编辑的核心其实不在API调用本身,而在"状态管理"——编辑对象是谁、什么时候关闭编辑器、数据从哪个环节取值、多个图形叠加时怎么让交互不混乱。把这四件事理清楚,高德地图上圆形和矩形的创建编辑就能变成一个很稳定、很顺手的功能模块。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询