项目中需要分析postGis库保存的空间数据,规划线路图。参考了很多博文,概要整理如下。
Java Topology Suite (JTS) 。音译为java拓扑套件。可见,咱们讨论的东西属于拓扑学范畴了,很高端。其实,说得通俗点,就是研究两个几何对象的空间关系。
比如二维平面中,一个正方形,分为:外部、内部、边线。3种情况。那么两个正方形的关系,就是3*3=9,即出现9种组合情形。这就是大名鼎鼎的九交模型。
我用的版本是1.13(com.vividsolutions.jts)。新版本的包是(org.locationtech.jts)。
API下部分包:
com.vividsolutions.jts.geom包:几何图形
com.vividsolutions.jts.linearref包:线性处理
com.vividsolutions.jts.noding包:计算交点
com.vividsolutions.jts.operation包:几何图形操作
com.vividsolutions.jts.planargraph包:平面图
com.vividsolutions.jts.polygnize包:多边形化
com.vividsolutions.jts.precision包:精度
com.vividsolutions.jts.util包:工具
1.计算两条线段相交的交点
GeometryFactory gf = new GeometryFactory();
WKTReader reader = new WKTReader(gf);
Geometry line1 = reader.read("LINESTRING(0 0, 10 10)");
Geometry line2 = reader.read("LINESTRING(0 10, 9 0)");
System.out.println("两线段相交于点:" + line1.intersection(line2));
2.计算线外一点到线段上最短距离以及投影点
GeometryFactory gf = new GeometryFactory();
WKTReader reader = new WKTReader(gf);
Geometry line2 = reader.read("LINESTRING(0 0, 10 0, 10 10, 20 10)");
Coordinate c = new Coordinate(5, 5);
PointPairDistance ppd = new PointPairDistance();
DistanceToPoint.computeDistance(line2, c, ppd);
System.out.println(ppd.getDistance() + ":" + ppd.getCoordinate(0));
3. 已知点在线段上投影点,截取子线段
GeometryFactory gf = new GeometryFactory();
WKTReader reader = new WKTReader(gf);
Geometry line2 = reader.read("MULTILINESTRING((113.21474651610197 28.188722190676344,113.21528655099482 28.188748142686563))");
Geometry pointJiaoDian = reader.read("POINT(113.21527081579211 28.18874745467764)");
LocationIndexedLine lil = new LocationIndexedLine(line2);
LinearLocation start = lil.indexOf(pointJiaoDian.getCoordinate());
LinearLocation end = lil.getEndIndex(); // 按自己的业务需要设定终点
Geometry result = lil.extractLine(start, end);
System.out.println(result.toText()); // 子线
4. 一些基本操作
翻转线段上的点:geometry.reverse();
合并两条线段:geom1.union(geom2);
点与点的距离:coordinate1.distance(coordinate2);
将WKT(well know text)转为几何图形:geometry = wktReader.read("MULTILINESTRING((113.21474651610197 28.188722190676344,113.21528655099482 28.188748142686563))");
1. maven依赖如下
com.baomidou
mybatis-plus-boot-starter
3.3.2
org.postgresql
postgresql
42.1.4
com.vividsolutions
jts
1.13
如果你想脱离postgis数据库的函数,算两个经纬度坐标点之间的实际距离,可以用下面这个包:
org.gavaghan
geodesy
1.1.3
代码算法:
import com.vividsolutions.jts.geom.Coordinate;
import org.gavaghan.geodesy.Ellipsoid;
import org.gavaghan.geodesy.GeodeticCalculator;
import org.gavaghan.geodesy.GlobalCoordinates;
public static double getDistance(Coordinate p1, Coordinate p2) {
GlobalCoordinates source = new GlobalCoordinates(p1.y, p1.x);
GlobalCoordinates target = new GlobalCoordinates(p2.y, p2.x);
return new GeodeticCalculator().calculateGeodeticCurve(Ellipsoid.WGS84, source, target).getEllipsoidalDistance();
}
2. yml配置如下
spring:
application:
name: demo
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/postgis
username: aa
password: 123456
mybatis-plus:
type-handlers-package: com.cs.dgt.hxd.handler # 自定义typehandler时,需要配置这个
mapper-locations: classpath*:mappers/**/*Mapper.xml
postgreSQL的默认端口是5432。不用mysql是由于postgreSQL对于自定义数据类型的优秀扩展性,所以postgis是基于postgreSQL来做的。
由于用到mybatis, 自定义typehandler,将postgis库中Geometry类型映射到java类型:
@MappedTypes(Geometry.class)
public class GeometryTypeHandler extends BaseTypeHandler {
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Geometry geometry, JdbcType jdbcType) throws SQLException {
PGobject pGobject=new PGobject();
pGobject.setValue(geometry.toText());
pGobject.setType("geometry");
preparedStatement.setObject(i,pGobject);
}
public Geometry getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
String geom = resultSet.getString(columnName);
if(geom==null){
return null;
}else{
WKTReader wktReader=new WKTReader();
try {
return wktReader.read(geom);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
}
public Geometry getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
String geom = resultSet.getString(columnIndex);
if(geom==null){
return null;
}else{
WKTReader wktReader=new WKTReader();
try {
return wktReader.read(geom);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
}
public Geometry getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String geom = callableStatement.getString(i);
if(geom==null){
return null;
}else{
WKTReader wktReader=new WKTReader();
try {
return wktReader.read(geom);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
}
}
pojo类中属性上,使用注解,将数据库类型映射为java 类型:
@TableField(typeHandler = GeometryTypeHandler.class)
private Geometry geom;
最后
分享我在已知由多条线段组成的有序清单下,拼接成一条最短规划线路的迭代算法。我觉得如果要做自动寻路,也不过是在未知有序清单时,根据道路权重等因素处理交叉路口,最后选取最短路程的那一条或者权重最合理的那一条罢了。
/**
* 迭代计算路径数据(将多段路径拼接。兼容相交时剪切最短路径、或者不相交时计算投影点最短路径)
* @param inputList 多条子线段的有序数据输入
* @param currIndex 当前处理到节点位置
* @param resultGeometry 数据输出
*/
private Geometry calculateShortestPath(List inputList, int currIndex, Geometry resultGeometry) {
assert !CollectionUtils.isEmpty(inputList);
if (inputList.size() == 1) { // 只有一条线,直接返回
return inputList.get(0).getGeom();
}
if (currIndex >= inputList.size()) {
return resultGeometry;
}
Hxd currHxd = inputList.get(currIndex);
// 获取上一线段的终点,作为当前线段的起点
Coordinate startPoint = null;
if (resultGeometry != null) {
Coordinate[] coordinates = resultGeometry.getCoordinates();
startPoint = coordinates[coordinates.length - 1];
}
// 获取下一条线段的起点,作为当前线段的终点
Coordinate endPoint;
if (currIndex == inputList.size() -1) {
// 当前为最后一条线段,则在线段的起点、终点中选距离远的一个点为终点
assert startPoint != null;
endPoint = calculateEndPointFarthest(startPoint, currHxd.getGeom());
} else {
// 计算 线与线 最近的点
endPoint = calculateNearestPoint(currHxd.getGeom(), inputList.get(currIndex + 1).getGeom());
}
if (resultGeometry == null) {
// 第一次,则在线段的起点、终点中选距离远的一个点作为起点
startPoint = calculateEndPointFarthest(endPoint, currHxd.getGeom());
}
// 添加截取的子线(线外一点也是可以截取子线的)
LocationIndexedLine lil = new LocationIndexedLine(currHxd.getGeom());
LinearLocation start = lil.indexOf(startPoint);
LinearLocation end = lil.indexOf(endPoint);
resultGeometry = resultGeometry == null ? lil.extractLine(start, end) : resultGeometry.union(lil.extractLine(start, end));
return calculateShortestPath(inputList, ++currIndex, resultGeometry);
}
/**
* 计算点 到 线两端的距离,返回距离较远的端点
* @param endPoint 点
* @param geometry 线
* @return 返回距离较远的端点
*/
private Coordinate calculateEndPointFarthest(Coordinate endPoint, Geometry geometry) {
Coordinate[] cs = geometry.getCoordinates();
Coordinate start = cs[0];
Coordinate end = cs[cs.length - 1];
return endPoint.distance(start) > endPoint.distance(end) ? start : end;
}
/**
* 线1与线2的交点,或线1上距离线2最近的点
* @param line1 线1
* @param line2 线2
* @return 线1与线2的交点,或线1上距离线2最近的点
*/
private Coordinate calculateNearestPoint(Geometry line1, Geometry line2) {
// 如果两条线段有交点,则取交点。
Geometry geo = line1.intersection(line2);
if (!geo.isEmpty()) {
return geo.getCoordinate();
}
// 如果没有交点,则取线段上距离最近的一个点
Coordinate[] cs = line1.getCoordinates();
Coordinate nearest = null;
Double distance = null;
PointPairDistance ppd = new PointPairDistance();
for (Coordinate coordinate : cs) {
DistanceToPoint.computeDistance(line2, coordinate, ppd);
if (distance == null || distance > ppd.getDistance()) {
nearest = coordinate;
distance = ppd.getDistance();
}
}
// log.info("line1={}, line2={}不相交,计算line1上距离line2最近点为:{}", line1.toText(), line2.toText(), nearest);
return nearest;
}