做项目优化的时候有个过滤逻辑耗时严重,因此写了个算法进行优化。
在此记录一下实现的过程和思路历程。
需求:有一条导航规划路线集合listA和一个poi点集合listB,所有点集合均为地图坐标,需要拿到在listA一定范围内所有poi点的集合,需求规定为3km。
当前业务逻辑是拿点listB与listA集合中每个点位用高德api进行距离计算,但是此方法耗时比较大,1000公里距离耗时达到了15分钟,10公里的耗时也有30秒左右,耗时惊人,所以我就自己写了个算法实现,结果为1000公里耗时2秒左右。
算法实现的初步想法:
点位的比较其实主要是计算listB中点位距离listA的距离是否在指定范围内。
所以我们从listB中取出一个点位C。
第一步:取点C上下左右3公里的经纬度。4个经纬度可以围成一个正方形,然后判断正方形里面是否存在listA集合中的点,如果正方形里面没有,则判断不在范围内,然后用正方形里面的poi点集合listPa进行第二步。
第二步:此时取点C为中心,指定范围的圆圈,判断范围内是否有listPa的点位,有则判断在范围内,没有则进行第三步。
第三步:此时listPa集合数据是正方形与圆形相切之后部分的位置里面,此时需判断listPa集合里面的点与其前后相邻点位的切线是否与圆相交,相交则判断在范围内。此过程中需要判断多个边界值问题,比如点位是否第一个或者最后一个,2个点位可能不在正方形内,但是切线在范围内,因为导航路径如果是一条直线可能只会给2个点,而poi点在直线的中间。
以上为分析内容,当然实际开发过程要复杂一点,相关图形分析后续补上。
以下为代码部分:
import java.util.ArrayList;
import java.util.List;
public class PolylineWorker {
private List mLine;
public PolylineWorker(List line) {
mLine = line;
init();
initLatLngD(filterDistance);
initPointDistance();
}
public void unInit() {
if (mPolyline != null) {
mPolyline.remove();
mPolyline = null;
}
}
/** 数据过滤半径 */
private float filterDistance;
/** 路径中2点间最大间隔距离值 */
private double pointMaxDistance;
/** 根据路径中2点间最大间隔距离计算得到的最大过滤范围值 */
private double filterMaxDistance;
/**
* 计算最近点距离--优化算法
*
* @return 结果为1则说明在范围内,为-1则说明超过距离
*/
public int nearestDistanceByFilter(LatLng latLng, float maxDistance) {
//初始化过滤数据量值
initLatLngD(maxDistance);
initPointDistance();
//过滤符合范围内坐标
List backFilter = filterByLatLng(latLng, latD, lngD);
//判断是否在半径内 1在半径内,-1不在半径内
int type = filterAfter(backFilter, latLng);
if (type == 1) {
return 1;
} else {
//如果不在半径内,则获取最大范围内路径点集合
List backs = filterByLatLng(latLng, latDMax, lngDMax);
//判断路径点集合中是否存在切线在范围内的点
return filterNearestPoint(backs, latLng);
}
}
/**
* 初始化路径中2点间最大间隔距离和经纬度偏移值
*/
private void initPointDistance() {
if (pointMaxDistance == 0) {
double dLat = 0;
double dLng = 0;
for (int i = 0; i < mLine.size(); i++) {
if (i < mLine.size() - 2) {
double tmpLat = Math.abs(mLine.get(i).latitude - mLine.get(i + 1).latitude);
double tmpLng = Math.abs(mLine.get(i).longitude - mLine.get(i + 1).longitude);
if (tmpLat > dLat) {
dLat = tmpLat;
}
if (tmpLng > dLng) {
dLng = tmpLng;
}
}
}
double disLat = filterDistance * dLat / latD;
double disLng = filterDistance * dLng / lngD;
pointMaxDistance = Math.max(disLat, disLng);
filterMaxDistance = Math.sqrt((Math.pow(filterDistance, 2) + Math.pow(pointMaxDistance, 2)));
Logger.e("最大偏移量距离:" + filterMaxDistance);
//初始化最大经纬度偏移值
initDlMax();
}
}
/**
* 初始化指定范围对应的经纬度偏移值
*
* @param maxDistance 指定的范围
*/
private void initLatLngD(float maxDistance) {
if ((latD == 0 && lngD == 0) || filterDistance != maxDistance) {
filterDistance = maxDistance;
initDl();
}
}
/**
* 使用圆圈半径的方式进行二次过滤
*/
private int filterAfter(List backFilter, LatLng latLng) {
for (LatLng one : backFilter) {
double tmpDistance = AMapUtils.calculateLineDistance(latLng, one);
if (tmpDistance < filterDistance) {
return 1;
}
}
Logger.e("filterAfter过滤之后数量:----" + backFilter.size());
return -1;
}
/**
* 如果没有符合要求的点,则需要校验是否为沿线点连线中附近距离以内
*/
private int filterNearestPoint(List tmpLine, LatLng latLng) {
if (tmpLine.size() == 0) {
return -1;
}
//根据指定范围点集合,判断是否存在满足条件的最近距离点距离
for (LatLng one : tmpLine) {
double dis = getNearestDistance(one, latLng);
if (dis < filterDistance) {
return 1;
}
}
return -1;
}
/**
* 根据给定点,判断点的前后连线距离过滤点的直线距离
*
* @param one 给定点
* @param latLng 过滤点
* @return 距离
*/
private double getNearestDistance(LatLng one, LatLng latLng) {
int sindex = mLine.indexOf(one);
if (sindex == 0) {
//起始点判断
LatLng right = mLine.get(sindex + 1);
return getDistanceByThreePoint(latLng, one, right);
} else if (sindex == mLine.size() - 1) {
//终点判断
LatLng left = mLine.get(sindex - 1);
return getDistanceByThreePoint(latLng, one, left);
} else {
//中间点判断
LatLng right = mLine.get(sindex + 1);
double rD = getDistanceByThreePoint(latLng, one, right);
LatLng left = mLine.get(sindex - 1);
double lD = getDistanceByThreePoint(latLng, one, left);
return Math.min(rD, lD);
}
}
/**
* 获取点距离另外2个点连线的距离
*
* @param latLng 被过滤点
* @param one 路径上的点
* @param other 路径左右的点
* @return 返回距离
*/
private double getDistanceByThreePoint(LatLng latLng, LatLng one, LatLng other) {
double c = AMapUtils.calculateLineDistance(latLng, one);
//防止经纬度因精度问题导致部分数据未检测到
if (c < filterDistance) {
return c;
}
double a = AMapUtils.calculateLineDistance(one, other);
double b = AMapUtils.calculateLineDistance(latLng, other);
double angleB = Math.acos(((Math.pow(a, 2) + Math.pow(c, 2) - Math.pow(b, 2)) / (2 * a * c))) * 180 / Math.PI;
double angleC = Math.acos(((Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2)) / (2 * a * b))) * 180 / Math.PI;
if (angleB > 90 || angleC > 90) {
//角度不符合要求,过滤掉
return filterDistance + 1;
} else {
//角度符合要求,计算切线距离
double P = (a + b + c) / 2;
double S = Math.sqrt((P * (P - a) * (P - b) * (P - c)));
return 2 * S / a;
}
}
/** 指定范围对应的经纬度偏差值 */
private double latD = 0;
/** 指定范围对应的经纬度偏差值 */
private double lngD = 0;
/** 最大偏差范围对应的经纬度偏差值 */
private double latDMax = 0;
/** 最大偏差范围对应的经纬度偏差值 */
private double lngDMax = 0;
/**
* 根据坐标经纬度进行第一次过滤
*/
private List filterByLatLng(LatLng poiItem, double latD, double lngD) {
List filterLine = new ArrayList<>();
for (LatLng one : mLine) {
if (Math.abs(one.latitude - poiItem.latitude) <= latD &&
Math.abs(one.longitude - poiItem.longitude) <= lngD) {
filterLine.add(one);
}
}
Logger.e("filterBefor过滤之后数量:----" + filterLine.size());
return filterLine;
}
/**
* 根据需要过滤的距离,计算出偏移指定距离所需要偏移的经纬度量
*/
private void initDl() {
double lngdis = AMapUtils.calculateLineDistance(new LatLng(0, 1), new LatLng(0, 0));
double latdis = AMapUtils.calculateLineDistance(new LatLng(1, 0), new LatLng(0, 0));
lngD = (filterDistance / lngdis) * 1;
latD = (filterDistance / latdis) * 1;
Logger.e("每3km经纬度偏移量结果:lat/lng--->" + latD + "/" + lngD);
}
/**
* 根据最大偏移的距离,计算出偏移指定距离所需要偏移的经纬度量
*/
private void initDlMax() {
double lngdis = AMapUtils.calculateLineDistance(new LatLng(0, 1), new LatLng(0, 0));
double latdis = AMapUtils.calculateLineDistance(new LatLng(1, 0), new LatLng(0, 0));
//一倍范围有遗漏概率,目前使用2倍最大偏移量
lngDMax = ((filterMaxDistance + pointMaxDistance) / lngdis) * 1;
latDMax = ((filterMaxDistance + pointMaxDistance) / latdis) * 1;
Logger.e("最大偏移量结果:lat/lng--->" + latDMax + "/" + lngDMax);
}
}