1.算法背景
目前好多的项目中都需要有线路规划的能力,这里有许多开源的地图SDK或者收费的SDK都可以实现,通过用户选取起点终点以及途径点规划出用户想要的路线,但识别路口的能力一般是不具备的,这里只是介绍一下我在项目开发中用到的算法来支持路口有识别以及对应的顺序。
2.基础环境
JDK版本:1.8
地图SDK: Dugis(百度离线地图属于收费SDK,这里只是用到路线规划能力,可以换成其他的地图SDK)
几何计算工具:JTS (Dugis本身具有几何计算的能力,但本算法计算较多,调用SDK耗时较长,而选取JTS在内存中计算)
3.规划线路识别路口
规划线路识别路口是基于已知区域内所有路口的点位坐标,规划线路经过路口很大概率不会经过路口的坐标点,毕竟路口坐标是个点,而真实路口是一个区域,本算法是定义一个阈值K(以路口坐标点以半径K缓冲的区域)来进行识别路口,进而推导出各个路口的坐标与规划的路线最近点的距离 S< K 就可以认为规划路线途径了该路口,这种方案适合绝大部分路口,但几种情况除外。
(1)路口实际较大,通过路口缓冲的区域无法覆盖整个路口;
(2)路口旁边有辅路路口,且与主路口是两个路口,在阈值K内;
(3)阈值K设置较大,其缓冲区域覆盖多个路口(非辅路路口);
解决以上问题就需要设置一个基础的阈值K和特殊路口特有的阈值K',其K'的优先级较高,在线路识别路口时区别计算;
具体算法步骤:
1.通过Dugis线路规划出一条完整的路线点集;
2.计算所有路口与该路线的最短距离;
这里使用的JTS工具计算,由于入参是坐标出参并不是距离,这里需要进行转换乘以一个系数C;
/**
* 角度转米系数
*/
private static final double C = 111120.0;
private static GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory( null );
private static WKTReader reader = new WKTReader(geometryFactory);
/**
* @Description 计算坐标点与线之间的最小距离
* @param pointKwt 点经纬度(WKT格式)
* @param lineKwt 线经纬度(WKT格式)
* @return java.lang.Double
*/
public static Double pointToLineDistance(String pointKwt, String lineKwt) {
try {
Point point = (Point) reader.read(pointKwt);
LineString line = (LineString) reader.read(lineKwt);
return DistanceOp.distance(point, line) * C;
} catch (ParseException e) {
log.error("pointToLineDistance ParseException:", e);
}
return Double.MAX_VALUE;
}
3.通过阈值K和K'进行筛选过滤符合要求的路口;
3.计算路口途径顺序
思路一:
规划的如果一条直线,那么途径路口顺序可以计算各个路口到起点路口的距离并按照距离递增顺序排序即线路途径路口顺序,其缺点是路线不能有拐弯,否则计算可能不准确;
思路二:
对思路一进行优化,由于规划路线时途径点一般都是为了约束路线,使其符合用户的需求才选择,大部分分布在拐点上。对整条线路进行途径点切分,每段路线中使用思路一的方法进行排序,最后进行整合去重,得到完整的途径路口顺序;
思路三:
各个区域的道路划分并不是一个横平竖直的道路,个别道路是个曲线或不规则的道路分布,计算点到点的直线距离无法得到正确的路口顺序,因而需要计算途径线路的曲线长度。这里引入JTS中的子线切分,即计算起点之外的所有路口距离路线最近点到起点路口之间曲线的长度,并排序进而得到对应路口的顺序。
/**
* 获取某个点到线中包含的点里最近的点
* @param point
* @param lineString
* @return
*/
public static Geolocation getNearestPointToLine(Geolocation point, LineString lineString) {
try {
Coordinate coordinate = new Coordinate(point.getLongitude(), point.getLatitude());
PointPairDistance ppd = new PointPairDistance();
DistanceToPoint.computeDistance(lineString, coordinate, ppd);
Coordinate[] coordinates = ppd.getCoordinates();
if (Objects.nonNull(coordinate) && coordinates.length > 0) {
Coordinate nearestPoint = coordinates[0];
return new Geolocation(nearestPoint.getX(), nearestPoint.getY());
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
获取子线是如下方案,具体需要结合自己项目中的业务进行嵌套,子线中会有对应的长度
GeometryFactory GeometryFactory = new GeometryFactory();
WKTReader reader = new WKTReader(GeometryFactory);
Geometry geom = reader.read("LINESTRING(0 0, 10 0, 10 10, 20 10)");
LocationIndexedLine lil = new LocationIndexedLine(geom);
LinearLocation start = lil.indexOf(new Coordinate(8, 5));
LinearLocation end = lil.indexOf(new Coordinate(17, 10));
Geometry result = lil.extractLine(start, end);
System.out.println(result.toText());
System.out.println("子线长度:" + result.getLength);
// 结果
LINESTRING (10 5, 10 10, 17 10)
思路四:
思路三中的路口识别顺序还是比较准确的,但如果一个路口途径两次或者多次就会出现途径路口顺序不准确的情况。计算路口坐标距离规划路线最近点只会有一个,无法识别出路口经过多次的情况,为了解决这种问题,这里引入了一个几何概念,当一条线经过一个圆,会与这个圆产生两个交点,经过两次会有四个交点(如果经过两次不出去会有三个交点),这样我们就知道了当交点次数N > 2时,说明路口经过多次。对交点进行子线切分并安子线长度排序得到了路口的途径顺序,但其中会有重复的路口,要进行去重,去重的原则是相邻的两个路口不能一致,即路口在隔一个或者多个路口可以出现重复,这样就得到了一个完整的路口途径顺序;但在实际开发中,将路口的坐标点以阈值K或者K'为半径进行缓冲得到的并不是一个正圆而是类似一个椭圆。在计算路线与缓冲区的交点时,得到的交点个数也不符合我们的设想;路线与缓冲区的交点次数N >= 2 ,缓冲区的里面也会出现交点,这个时候可以考虑两种解决方案。
方案一:将线与缓冲区所有交点进行子线切分并排序去重。
方案二:获取缓冲区的外边界,这是符合我们的设想,经过一次出现两个交点,对交点次数N > 2的路口进行交点子线切分排序去重,而N == 2的路口不予考虑;
以上就是我本次的分享,欢迎诸位大佬有新的算法来计算识别路口顺序在评论区留言,最后附上我的部分开发代码,其中对象中会有一些其他属性数据,并不影响算法思路;
/**
* @Description 最新计算路线途径路口(支持多次经过路口)
* @param pointList 符合阈值K或者K'的路口列表--无序
* @param lineStr 规划路线的WKT格式字符串
* @param passPoint 途径点列表(首尾为起终点路口,期间为途径点 size >= 2)
* @param coverThreshold 阈值K(这里没有配置K',如果需要可以安路口获取K)
*/
public static List<LinePointVO> calculationCrossSeq(List<DirectionDTO> pointList, String lineStr,
List<LinePointVO> passPoint, Double coverThreshold) {
LinePointVO startCross = passPoint.get(0);
LinePointVO endCross = passPoint.get(passPoint.size() - 1);
LocationIndexedLine line;
Geometry geom;
try {
geom = reader.read(lineStr);
line = new LocationIndexedLine(geom);
} catch (ParseException e) {
log.error("calculationCrossSeq ParseException:", e);
throw new RuntimeException("calculationCrossSeq ParseException:" + e);
}
Geolocation startPoint = startCross.getGeolocation();
LinearLocation start = line.indexOf(new Coordinate(startPoint.getLongitude(), startPoint.getLatitude()));
List<DirectionDTO> subLineLengts = Lists.newArrayList();
for (DirectionDTO dto : pointList) {
Geometry buffer = createPointBuffer(dto.getPoint(), coverThreshold);
Geometry intersections = geom.intersection(buffer);
for(Coordinate coord : intersections.getCoordinates()){
DirectionDTO intersect = new DirectionDTO();
BeanUtils.copyProperties(dto, intersect);
LinearLocation end = line.indexOf(new Coordinate(coord.x, coord.y));
Geometry extractLine = line.extractLine(start, end);
Double dis = extractLine.getLength();
intersect.setDistance(dis);
subLineLengts.add(intersect);
}
}
subLineLengts = subLineLengts.stream().sorted(Comparator.comparing(DirectionDTO::getDistance))
.collect(Collectors.toList());
pointList = removeAdjacentToSameCross(subLineLengts);
List<LinePointVO> list = pointList.stream().sorted(Comparator.comparing(DirectionDTO::getDistance)).map(o -> {
LinePointVO vo = new LinePointVO();
vo.setCrossId(o.getCrossId());
vo.setCrossName(o.getName());
vo.setCrossType(o.getCrossType());
vo.setGeolocation(WKTUtil.point2location(o.getPoint()));
return vo;
}).collect(Collectors.toList());
List<LinePointVO> resList = Lists.newArrayList();
if (!startCross.getCrossId().equals(list.get(0).getCrossId())) {
resList.add(startCross);
}
resList.addAll(list);
if (!endCross.getCrossId().equals(resList.get(resList.size() - 1).getCrossId())) {
resList.add(endCross);
}
return resList;
}
/**
* @Description 坐标点根据半径缓冲成一个面(多边形-类似一个椭圆)
* @param point 坐标点-WKT格式
* @param r 半径-米
*/
public static Geometry createPointBuffer(String point, double r) {
try {
BigDecimal factor = new BigDecimal(Double.toString(C));
BigDecimal radius = new BigDecimal(Double.toString(r));
double distance = radius.divide(factor, 10, BigDecimal.ROUND_HALF_UP).doubleValue();
Geometry geomPoint = reader.read(point);
Geometry buffer = geomPoint.buffer(distance);
return buffer;
} catch (ParseException e) {
throw new RuntimeException("createPointBuffer ParseException:" + e);
}
}
/**
* @Description 坐标点根据半径缓冲成一个环线
* @param point 坐标点-WKT格式
* @param r 半径-米
*/
public static Geometry createPointBufferToLineRing(String point, double r) {
try {
BigDecimal factor = new BigDecimal(Double.toString(C));
BigDecimal radius = new BigDecimal(Double.toString(r));
double distance = radius.divide(factor, 10, BigDecimal.ROUND_HALF_UP).doubleValue();
Geometry geomPoint = reader.read(point);
Geometry buffer = geomPoint.buffer(distance);
String ring = WKTUtil.polygon2Linearring(buffer.toString());
Geometry lineRing = reader.read(ring);
return lineRing;
} catch (ParseException e) {
throw new RuntimeException("createPointBufferToLineRing ParseException:" + e);
}
}
/**
* @Description 去除相邻两个一样的路口
* @param list
*/
private static List<DirectionDTO> removeAdjacentToSameCross(List<DirectionDTO> list) {
List<DirectionDTO> resList = Lists.newArrayList();
for (int i = 0; i < list.size(); i++) {
if (CollectionUtils.isEmpty(resList) ||
!resList.get(resList.size() - 1).getCrossId().equals(list.get(i).getCrossId())) {
resList.add(list.get(i));
}
}
return resList;
}