使用JTS与postgis进行空间数据交互

4,712 阅读5分钟

使用JTS与postgis进行空间数据交互

上篇文章在java中使用postgis操作地理位置数据简单说明了基本的postgis建模,还有其如何与java程序进行数据交互。 但postgis-jdbc中提供的java模型生态与通用行不好,在java生态中,还有一个专门进行几何运算的库JTS

JTS Topology Suite(JTS)拓扑套件是开源Java软件库,它提供平面几何的对象模型以及一组基本几何功能。并且 JTS符合Open GIS联盟发布的SQL简单功能规范(Simple Features Specification for SQL)。所以JTS 不仅可以和postgis的数据进行交互,并且还可以在java层提供空间数据关系的运算。

下面会介绍下怎样在一个java项目中引入JTS并与postgis中的数据进行交互

1 环境说明

  • jdk11
  • gradle6
  • postgres11+postgis
  • idea 2019.3

2 预备工作

同上编文章,这里不赘述

3 准备数据

这里也和上篇一致,不赘述。表结构为

CREATE TABLE "t_gps"
(
    "time"     timestamptz(3)        NOT NULL,
    "dev_id"   varchar(36)           NOT NULL,
    "location" geography(Point, 4326) NOT NULL,
    "gps_num"  int4,
    "gps_type" varchar(10)           NOT NULL,
    "azimuth"  float4,
    "gnd_rate" float4
) WITHOUT OIDS;

4 java后端项目

项目的基本结构还是spring-boot + mybatis-plus,下面介绍下其它工作与要点

4.1 引入JTS

使用gradle,直接引入jts-core即可

implementation "org.locationtech.jts:jts-core:1.16.1"

其中核心是org.locationtech.jts.geom.Geometry类,可以看到其结构与postgis的数据类型是基本一致的

img

4.2 java实体建模

这里建模也基本与上次一致,但要注意location字段的类型已经变为了org.locationtech.jts.geom.Point

@Data
@TableName("t_gps")
public class GpsEntity {
    @JsonFormat(shape = JsonFormat.Shape.STRING,
            pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
            timezone = "+08")
    @ApiModelProperty("时间")
    private Date time;
    @ApiModelProperty("设备id")
    private String devId;
    @ApiModelProperty("位置")
    private Point location;
    @ApiModelProperty("卫星定位数")
    private int gpsNum;
    @ApiModelProperty("GPS定位信息")
    private String gpsType;
    @ApiModelProperty("对地真北航向角")
    private double azimuth;
    @ApiModelProperty("地面速率")
    private double gndRate;
}

4.2 jts与jdbc交互

postgis-jdbc项目中,本来对JTS有支持,但由于其太久没有维护,包名都不对了…… 这里只能自行修改其代码了。

修改后的代码可以参考源码中的00-postgis-jdbc-jts,一个是修改了老的JTS依赖与其包名(JTS项目更变过包名), 其二是添加了[postgis]中的geography类型的支持。

    /**
     * Adds the JTS/PostGIS Data types to a PG Connection.
     * @param pgconn The PGConnection object to add the types to
     * @throws SQLException when an SQLException occurs
     */
    public static void addGISTypes(PGConnection pgconn) throws SQLException {
        pgconn.addDataType("geometry", JtsGeometry.class);
        //这里添加geography类型
        pgconn.addDataType("geography", JtsGeometry.class);
    }

然后在spring-boot的配置中,还需要修改下jdbc配置

spring:
  datasource:
    #这里使用的驱动不一样!,可以自动添加jts的各种类型
    driver-class-name: org.postgis.jts.JtsWrapper
    #url也不一样
    url: jdbc:postgres_jts://localhost:5432/jts-test

然后为了让mybatis支持JTS的数据类型,还是需要自定义TypeHandler

public abstract class AbstractJtsGeometryTypeHandler<T extends Geometry> extends BaseTypeHandler<T> {

    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, new JtsGeometry(parameter));
    }

    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        JtsGeometry jtsGeometry = (JtsGeometry) rs.getObject(columnName);
        if (jtsGeometry == null) {
            return null;
        }
        return (T) jtsGeometry.getGeometry();
    }

    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        JtsGeometry jtsGeometry = (JtsGeometry) rs.getObject(columnIndex);
        if (jtsGeometry == null) {
            return null;
        }
        return (T) jtsGeometry.getGeometry();
    }

    public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        JtsGeometry jtsGeometry = (JtsGeometry) cs.getObject(columnIndex);
        if (jtsGeometry == null) {
            return null;
        }
        return (T) jtsGeometry.getGeometry();
    }

}
//下面是具体的各个类型
@MappedTypes(LinearRing.class)
public class JtsLinearRingTypeHandler extends AbstractJtsGeometryTypeHandler<LinearRing> {
}

@MappedTypes(LineString.class)
public class JtsLineStringTypeHandler extends AbstractJtsGeometryTypeHandler<LineString> {
}

@MappedTypes(MultiLineString.class)
public class JtsMultiLineStringTypeHandler extends AbstractJtsGeometryTypeHandler<MultiLineString> {
}

@MappedTypes(MultiPoint.class)
public class JtsMultiPointTypeHandler extends AbstractJtsGeometryTypeHandler<MultiPoint> {
}

@MappedTypes(MultiPolygon.class)
public class JtsMultiPolygonTypeHandler extends AbstractJtsGeometryTypeHandler<MultiPolygon> {
}

@MappedTypes(Point.class)
public class JtsPointTypeHandler extends AbstractJtsGeometryTypeHandler<Point> {
}

@MappedTypes(Polygon.class)
public class JtsPolygonTypeHandler extends AbstractJtsGeometryTypeHandler<Polygon> {
}

到这里,就已经可以进行数据交互了

@Mapper
public interface GpsRepo extends BaseMapper<GpsEntity> {
}

......

@Autowired
GpsRepo gpsRepo;

@Test
public void testReadData() {
    QueryWrapper<GpsEntity> wrapper = new QueryWrapper<>();
    wrapper.orderByAsc("time");
    wrapper.eq("dev_id", "0004r");
    wrapper.last("limit 100");
    List<GpsEntity> list = gpsRepo.selectList(wrapper);
    for (GpsEntity entity : list) {
        System.out.println(entity);
    }
}

img

4.3 在rest接口中输出GeoJson格式的数据

GeoJson是一种用于编码各种地理数据结构的格式。广泛运用与各种gis分析软件与gis库中, 如果接口能直接返回geojson,对于前端开发也可以减少大量的工作。

本来有一个jackson-datatype-jts 的库可以完成org.locationtech.jts.geom.GeometryGeoJson的转换,但又没人维护了……

所以在源码中,升级了jts依赖、并修改包名后,将其引入

//这是spring-boot添加jackson module的方法,如果不是spring-boot
//应该还是需要自行注册jackson module
@Configuration
public class JacksonConfig {

    ......
    @Bean
    public JtsModule jtsModule(){
        return new JtsModule();
    }
}

然后我们实现一个将gps轨迹由point转为linestring的接口

    @GetMapping("/line")
    public GpsLine line(
            @NotEmpty
            @RequestParam(value = "devId", required = false)
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) String devId,
            @RequestParam(value = "bTime", required = false)
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date bTime,
            @RequestParam(value = "eTime", required = false)
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date eTime) {

        List<GpsEntity> history = gpsService.history(devId, bTime, eTime);
        GpsLine gpsLine = new GpsLine();
        gpsLine.setDevId(devId);
        gpsLine.setStart(bTime);
        gpsLine.setEnd(eTime);

        Coordinate[] points = history.stream()
                .map(entity -> entity.getLocation().getCoordinate())
                .toArray(Coordinate[]::new);

        gpsLine.setLine(JtsUtil.geometryFactory4326.createLineString(points));
        return gpsLine;
    }

通过调用会返回这样的结果

{
  "start": null,
  "end": null,
  "devId": "0004r",
  "line": {
    "type": "LineString",
    "coordinates": [
      [106.87683,27.52788],[106.87682,27.52788].....
     ]
  }
}

这里line便是GeoJson类型的数据,我们将其保存下来放入QGIS中可以查看其图像

首先在QGIS中添加一个矢量图层

img

在面板中,选择类型为GeoJson,统一资源标识符(就是URI……)选择刚才保存的文件,当然既然 是URI,使用http url也可以,但这里我们还是先使用文件。

img

导入后,选择对于图层,即可看到对应的轨迹

img

将其它图层与底图也选中,可以很清晰看到轨迹与gps坐标点的关系

img

5 总结

本文完成了jts与postgis的交互,又实现了geojson输出几何图形数据,基本的数据交互已经完结了。 后面的文章会介绍如何通过postgis来分析地理位置数据间的关系,然后如何在java中结合mybatis来 调用它们。