在百度地图搜索驾车路线的时候,用户可以在结果路线上进行拖拽,使路线按照用户的意愿进行调整。 当用户将鼠标移至路线上时,在路线上
会出现一个圆圈和一些文字信息,提示用户可以拖拽当前这个位置,本文介绍了此功能目前的一些问题以及新的解决方案。
现有的问题
在百度地图展示驾车路线结果时,用户可以通过拖动道路上的某个点来实现自定义途经点的功能。如下图所示:
当鼠标移至路线附近时,路线上会有一个黑色边框的圆圈出现,用来提示用户可以拖动此点到他需要经过的地点。 目前的做法是当鼠标移至线路上时,处理线对象的 mouseover 事件,同时得到鼠标当前的位置,此时将这个圆圈显示在鼠标所在的位置。当鼠标离开线路时,响应 mouseout 事件,事件监听函数中在隐藏那个圆圈。 这样做实现起来非常容易,但是导致了两个问题:
一: 圆圈出现的位置是由鼠标所在位置决定的,有时候并不能出现在道路的中心位置,而是有些偏移。 理想状况下,圆圈应该始终位于路线中心,不应受鼠标位置的影响。否则就会出现如下图所示的情形:
二: 鼠标在线路上移动时由于频繁响应 mouseover 和 mouseout 事件,导致圆圈的移动效果不平滑。
改变思路
为了解决以上两个问题,可换另一种思路来解决这个问题:不响应 mouseover 和 mouseout 事件,而是监听 mousemove 事件,判断鼠标的位置是否与线路足够接近,即点到线的距离是否足够小,如果是则找到此时线路上那个最小距离所对应的位置并显示圆圈,否则隐藏圆圈。
实现方案
为了实现上述方法,首先我们要求出点到路线的最短距离。路线是由若干条线段组成的,只要求出点到每一条线段的距离,再从中找到那个最小值即可。
基本数学知识
高中的时候我们学过点到直线的距离该如何计算,距离公式为:
点 P(x0 , y0 ) 到直线 l : Ax + By + C = 0 的距离为:
但是我们的线路并不是无限延伸的直线,而是长度有限的线段,因此我们需要得到点到线段的距离。
在上图中,我们将线段周围的空间划分为两个部分:区域 1 和区域 2 。图中红色线段的长度即为点到线段的距离。可以看出,当点位于区域 1 时,点到线段的距离可通过点到直线的距离公式来计算;当点位于区域 2 时,点到线段的距离为: min(d1, d2) , 其中 d1 和 d2 分别是点到线段两个端点的距离。
那么如何判断点是在区域 1 中还是区域 2 中呢?可以用下图的方法来计算:
位于区域 1 中的点 A 向线段做垂线,垂线与线段的焦点到线段的两个端点的距离分为 d1 和 d2 ,如果线段的长度为 d ,则此时有: d1 + d2 = d 。 点 B 位于区域 2 ,从点 B 向线段的延长线做垂线,焦点到线段的两个端点的距离也设为 d1 和 d2 ,如果线段的长度为 d ,则此时有: d1 + d2 > d 。 由此我们就可以区分出位于不同区域点,以便采用不同的方法计算点到线段的距离。
代码实现
好了高中数学回顾就告一段落,现在我们需要通过 JavaScript 来实现,假定折线上所有点存放于 routePixels 数组中,根据这些点我们需要得到每条线段的方程。
直线方程
已知两点求直线方程可以用两点式,这里我们用斜截式: y = kx + b 。首先计算斜率:
var k = (routePixels[n].y - routePixels[n-1].y) / (routePixels[n].x - routePixels[n-1].x); // 斜率
var b = routePixels[n].y - k * routePixels[n].x;
点到直线距离
k 和 b 已经求得,我们需要把斜截式变换为标准式以便计算距离。 y = kx + b 变换为: kx - y + b = 0 (即 A 为 k , B 为 -1 , C 为 b ),带入到距离公式则有(其中 mousePx 为鼠标位置对象,包含 x 和 y 属性):
var dist = Math.abs(k * mousePx.x - mousePx.y + b ) / Math.sqrt(k * k + 1);
点和线段的位置关系
下面就要判断点和线段之间的位置关系了,即点位于区域 1 还是区域 2 。我们要计算出 d1 和 d2 的值,把它与 d 进行比较。下图标出了代码中需要计算的各个部分:
// 线段长度 d 的平方
var d2 = Math.pow(routePixels[n].y - routePixels[n-1].y, 2) + Math.pow(routePixels[n].x - routePixels[n-1].x, 2);
// 点到线段端点 1 的距离的平方
var la2 = Math.pow(routePixels[n].y - mousePx.y, 2) + Math.pow(routePixels[n].x - mousePx.x, 2);
// 点到线段端点 2 的距离的平方
var lb2 = Math.pow(routePixels[n-1].y - mousePx.y,2) + Math.pow(routePixels[n-1].x - mousePx.x, 2);
// 点到直线距离的平方
var dist2 = Math.pow(dist, 2);
// 计算 d1 的平方加 d2 的平方
var calc = la2 - dist2 + lb2 - dist2;
// 比较 calc 与 d 的值,这里直接比较它们的平方值即可
if (calc > d2){
// 点位于区域 2 ,则重新修正 dist ,取 min(d1, d2)
dist = Math.sqrt(Math.min(la2, lb2));
}
特殊情况考虑
线路中的某段道路可能是水平或垂直的,即该线段的斜率为 0 或者为 1 ,此时计算距离需用如下方式:
// 线段水平时
var dist = Math. abs (mousePx.y - routePixels[n].y)
// 线段垂直时
var dist = Math. abs (mousePx.x - routePixels[n].x)
计算圆圈的位置
我们将线路上的所有线段进行遍历,依次计算出 dist 值,找到其中最小的那个。接着要在线路上找到那个产生最小距离的点的位置,把圆圈画在那个位置即可。 当点位于区域 1 中,从鼠标点的位置向线段坐垂线,垂线与线段的交点就是这个位置,当点位于区域 2 中,这个位置就是线段的某一个端点。下图的红点表示了该点的位置。
用 secX 和 secY 表示这个点的 x 坐标和 y 坐标,则有:
// 线段水平时
var secX = mousePx.x;
var secY = routePixels[n].y;
// 线段垂直时
var secX = routePixels[n].x;
var secY = mousePx.y;
// 任意时且点位于区域 1 ,计算垂线方程,求交点
var ca = -( 1 / k);
var cb = mousePx.y - ca * mousePx.x;
var secX = (cb - b) / (k - ca);
var secY = k * secX + b;
// 任意时且点位于区域 2
if (la2 < lb2){
var secX = routePixels[n].x;
var secY = routePixels[n].y;
}
else {
var secX = routePixels[n - 1 ].x;
var secY = routePixels[n - 1 ].y;
}
结论
通过对比可以发现,新方法的道路捕捉平滑性更好,圆圈也能一直显示在道路的中心位置。 此方法的性能主要由道路上的点的数量所决定,因此寻找一个较好的道路抽稀算法以减少点的数量对性能提高也是很有帮助的。 目前在 IE 下的平滑效果还不够理想,需要进一步的改进。