背景
某天,业务突然找到正在摸鱼🐟的我,说想要一个工具,支持在高德地图上绘制矩形,并且可以编辑矩形。
我:(内心os:这玩意不是高德本来就支持吗?还要做?)老实的打开高德官方demo😏,说这些不是现成支持吗?高德矩形绘制和编辑
产品:我看过高德的矩形编辑了,不是我想要的,我要的是可以对4个点进行拖拽编辑,还要支持可以把矩形旋转任意角度,还要可以查看矩形的单边距离。
我:啊这...你说的这些高德好像都不支持,这些都要开发的😲
产品:我不管,大大大boss要的,我相信你一定可以搞出来的(撂下话,马上跑了)
我:...
目标
既然这么被信任(我瞟了一眼脖子上的亮光),那就老实的干吧;最后通过调研高德的一些能力另辟蹊径的实现了这个工具。
最终效果(支持编辑、旋转,测距在非编辑下支持)
技术方案
查看了高德的矩形绘制和编辑,发现能实现基础的矩形绘制和编辑,但是高德绘制的矩形,只能通过西南点和东北点绘制矩形,这样子就实现非垂直于x轴和y轴的矩形。考虑到这,想到多边形的能力比较齐全,即支持编辑也可以绘制任意方向的图形,脑子里突然多了一个想法,把这个魔改一下如何😆。
确认方案:
查看时
用多边形来绘制矩形。
编辑时
用多边形绘制的矩形,来作为数据源,进行编辑,在控制一下多边形的编辑行为,来达到矩形的编辑。
绘制矩形
通过多边形来绘制矩形:
方法一:四点创建矩形
比较简单,直接给多边形传递4个点的路径来创建矩形;
js
const likeRectange = new AMap.Polygon({ path: [p1, p2, p3, p4] });
通过四点创建矩形,可以创建任意方向的矩形(因为本质还是创建多边形)。
方法二:两点创建矩形
通过 西南点经纬度(x₁, y₁) 和 东北点经纬度(x₂, y₂) 两点创建矩形;
通过计算,可以得到矩形的4个点:左上点:(x₂, y₁)、右上点:(x₂, y₂)、右下点:(x₂, y₁)、左下点:(x₁, y₁) ,然后在通过方法一创建矩形;
方法三:中心点+宽高创建矩形
设中心点经纬度(lng, lat),矩形宽(200) x 高(50)。
计算4个点坐标:
js
const center = AMap.LngLat(lng, lat);
const widht = 200;
const height = 50;
const leftTopPoint = center.offset(-width/2, height/2); // 左上点
const rightTopPoint = center.offset(width/2, height/2); // 右上点
const rightBottomPoint = center.offset(width/2, -height/2); // 右下点
const leftBottomPoint = center.offset(-width/2, -height/2); // 左下点
得到4个点坐标,在通过方法一创建矩形。
矩形能力扩展
矩形的绘制功能已经支持了,接下来就是扩展矩形的一些能力了。
边长测距
实现思路:此时已经矩形的4个顶点的坐标,可以通过两两连线,得到4条直线,然后在地图上绘制4条不可见的线条(实际是通过高德的工具方法来判断),添加鼠标移入事件,触发两点距离计算。
js
// 地图实例
const mapIns = new AMap.Map({ ... });
// 矩形实例
const likeRectangle = new AMap.Polygon({ path: [p1, p2, p3, p4] });
// 矩形4个点坐标
const paths = likeRectangle.getPath();
// 记录矩形4条直线的两个端点
let idx = 0;
const len = paths.length;
const linesPath = [];
while (idx < len) {
const nextIdx = idx + 1 >= len ? 0 : idx + 1;
linesPath.push([paths[idx], paths[nextIdx]]);
idx++;
}
// 注册地图鼠标事件
mapIns.on('mousemove', (event) => {
const { lnglat } = event;
// 判断是否在两点的连线上
const line = linesPath.find(path => {
return AMap.GeometryUtil.isPointOnSegment(lnglat, path[0], path[1])
});
// 存在,即表示当前鼠标点在直线上,则显示提示文案
if (line) {
new AMap.Text({ text: 'x米', position: lnglat, map: mapIns });
}
});
实现效果
矩形编辑
编辑能力因为是直接用多边形的矩形,故可以直接用多边形的编辑功能。但需要额外处理多边形编辑时的中间点,需要将这些中间点隐藏掉。
隐藏中间点
从多边形的编辑参数可以知道,中间点样式配置继承于 CircleMarkerOptions,可以把 radius
设置为 0
来达到隐藏的效果😆。
编辑点(白色点)行为控制
多边形编辑点默认行为:
- 点击编辑点,会取消该编辑点
- 编辑点支持的
adjust
事件 (ps: 这里有一个坑,adjust
只支持在编辑点移动完成后响应😬,即无法监控编辑点的开始、移动、结束行为)
上述两个默认行为比较致命,如果无法解决这两个问题,就不能得到一个可靠的矩形编辑功能,所以只能各个击破。
魔改
针对『点击编辑点,会取消该编辑点』,从编辑点参数可以知道,编辑点应该是通过 CircleMarker
来实现的,我们可以通过拿到编辑点实例,然后把默认的点击事件去移除。
js
// 创建编辑器实例
const likeRectangeEditorIns = new LikeRectangleEditor(likeRectangle, { ... });
// 拿到编辑点(在实例找拿)
const editorPoints = [p1, p2, p3 ,p4];
// 移除默认的点击行为
editorPoints.forEach(point => point.clearEvents('click'))
针对『编辑点支持的 adjust
事件』,一种思路类似通过修改 CircleMarker
的事件行为来实现;另一种思路则可以通过地图点击事件 + 是否点击到编辑点上等组合逻辑(实际逻辑比较复杂),在实现开始、移动、结束事件的监听。
具体实现可以去查看源码。
相邻两点位置实时计算
这里就要用到初中知识了(忘记的同学还可以补习一波~)
简单点的描述就是求平面坐标内的两条互相垂直的交点(特性:两条互相垂直的直线,斜率乘积 = -1)。
矩形旋转
首先,高德地图并不支持矢量图形旋转的功能,所以这块需要自己实现。
这里可以拆解成两步来实现:
1、实现旋转能力;
2、根据旋转角度,计算出4个顶点新的经纬度。
旋转能力
这里直接用了一个三方库 moveable
来实现了,不过多介绍。
大致思路是通过 moveable
控制旋转,然后实时更新旋转后的点位。
旋转后的点位的经纬度计算
js
// 矩形的中心点,也是矩形的旋转点
const centerPoint = new AMap.LngLat(cLng, cLat);
// 将经纬度转为平面坐标
const centerPointPixel = mapIns.lngLatToContainer(centerPoint);
// 左上点
const leftTopPoint = new AMap.LngLat(lng, lat);
// 将经纬度转为平面坐标
const leftTopPointPixel = mapIns.lngLatToContainer(leftTopPoint);
// 求点P(x1,y1)绕另一个点O(x2,y2)旋转θ度的坐标
// 计算公式:
// x = (x1 - x2) * cos(θ * Math.PI / 180) - (y1 - y2) * sin(θ * Math.PI / 180) + x2
// y = (x1 - x2) * sin(θ * Math.PI / 180) - (y1 - y2) * cos(θ * Math.PI / 180) + y2
const calcRotatePoint = (point: AMap.Pixel, center: AMap.Pixel, angle: number) => {
const x1 = point.x;
const y1 = point.y;
const x2 = center.x;
const y2 = center.y;
const angleRad = (angle * Math.PI) / 180;
const dx = x1 - x2;
const dy = y1 - y2;
const newX = dx * Math.cos(angleRad) - dy * Math.sin(angleRad) + x2;
const newY = dx * Math.sin(angleRad) + dy * Math.cos(angleRad) + y2;
return { x: newX, y: newY };
}
// 然后将新的平面坐标转位经纬度坐标
const nextLeftTopPointPixel = calcRotatePoint(leftTopPointPixel, center, 30);
// 左上点旋转 θ 度后的坐标
const nextLeftTopPoint = mapIns.containerToLngLat(new AMap.Pixel(nextLeftTopPointPixel.x, nextLeftTopPointPixel.y));
// 通过同理计算其他点位
矩形拖动
多边形本身支持了拖动能力,我们需要特殊处理的是在拖动的时候,同步更新矩形的中心点、四个顶点位置以及旋转点的坐标。
js
likeRectangle.on('dragend', () => {
// 拖动完成后,更新矩形
...
// 更新矩形中心点
...
// 更新
...
});å
后续优化
动态测距: 是指矩形在编辑点操作时,可以实时计算临接的两个动点之间的实时距离。
边界控制: 目前高德的矩形可能,有一个最小的绘制控制,以及当前操作点不可移动到相邻两点外。
指定旋转中心: 目前只支持绕矩形的中心点进行旋转,后续支持可以指定旋转中心进行旋转。