JTS使用笔记

项目中需要分析postGis库保存的空间数据,规划线路图。参考了很多博文,概要整理如下。

JTS简介

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包:工具

JTS点、线之间关系计算

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))");

整合springboot+postGis+JTS

1. maven依赖如下

<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<version>42.1.4</version>
		</dependency>
		<dependency>
			<groupId>com.vividsolutions</groupId>
			<artifactId>jts</artifactId>
			<version>1.13</version>
		</dependency>

如果你想脱离postgis数据库的函数,算两个经纬度坐标点之间的实际距离,可以用下面这个包:

<dependency>
			<groupId>org.gavaghan</groupId>
			<artifactId>geodesy</artifactId>
			<version>1.1.3</version>
</dependency>

代码算法:

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<Geometry> {
    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<Hxd> 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;
    }

猜你喜欢

转载自blog.csdn.net/kaiyuantao/article/details/128922445