在java中使用postgis操作地理位置数据
最近要做gps数据的分析,开始学习postgis。首先推荐下不睡觉的怪叔叔的专栏,
里面的postgis教程可以说很全面了,我就是靠这入的坑。
但在java程序中,对gis数据进行操作还有些问题,这里简单用文章记录下。
demo源码在https://gitee.com/Lonelyleaf/postgis-java-demo,下面简单说明下整个过程与遇到的坑
1.环境说明
- jdk11
- gradle6
- postgres11+postgis
- idea 2019.3
2.预备工作
首先需要安装postgres与postgis,在windows下,安装postgres时可以顺带安装postgis,记得勾选

安装好后,使用pgadmin来新建一个数据库,这里就叫做gis-test


然后选中你的数据库,选择上方的Tools -> Query Tool打开查询编辑器。

输入以下sql来启用PostGIS
create extension postgis
数据库就准备好了!
3.准备数据
如果直接使用我的源码,那么启动项目会自动建立表结构与初始数据。这里还是说明下表结构与数据:
-- 创建gps数据表
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;
COMMENT ON COLUMN "t_gps"."time" IS '时间';
COMMENT ON COLUMN "t_gps"."dev_id" IS '设备ID';
COMMENT ON COLUMN "t_gps"."gps_num" IS '卫星定位数';
COMMENT ON COLUMN "t_gps"."gps_type" IS 'GPS定位信息';
COMMENT ON COLUMN "t_gps"."azimuth" IS '对地真北航向角';
这里建立一个gps数据表,其中location字段使用的GEOGRAPHY来保存坐标信息。
其中Point表示是点信息,postgis还支持Linestring、Polygon等。
后面的4326表示了SRID,表明了使用的哪种坐标系,这里是使用的WGS84,既gps的
标准坐标。
然后在这里 下载样本数据的sql,放入数据库中执行。
执行成功后选中数据库,然后Schemas->Tables在t_gps表右键View/Edit Data->All Rows就能看到刚才导入的数据了,

然后可以看到下面的Data Output选项卡中,我们的location字段右边有一个👁一样的图标,点击后,可以简单的在地图上
预览到刚才我们导入的数据


要专门分析数据,可以用qgis或arcgis这些专业的gis软件,这里我们已经初步达成目的,下面说明下java后端程序中,怎样
读写postgis数据。
最后给location建立索引,注意要使用gist索引来创建。
create index idx_gpt_location on t_gps using gist("location");
postgis的文档上对gist索引有介绍,时专门针对空间数据的一种索引,在
4.java后端工程
具体项目请参考我的项目源码, 基本是按照spring boot的curd工程来搭建。由于用到很多其它技术具体搭建过程这里不细说, 下面简单说明下读写数据的过程和一些痛点。
4.1 java表实体
首先项目使用的是mybatis-plus来做crud,下面是t_gps表的java实体:
@Data
@TableName("t_gps")
public class GpsEntity {
@ApiModelProperty("时间")
private Date time;
@ApiModelProperty("设备id")
private String devId;
@ApiModelProperty("位置")
private org.postgis.Point location;
@ApiModelProperty("卫星定位数")
private int gpsNum;
@ApiModelProperty("GPS定位信息")
private String gpsType;
@ApiModelProperty("对地真北航向角")
private double azimuth;
@ApiModelProperty("地面速率")
private double gndRate;
}
4.2 postgis的geometry、geography类型问题
注意org.postgis.Point在postgis-jdbc中,并且为了让jdbc能正确读取数据,需要
将postgis-jdbc中的数据进行注册。postgis-jdbc提供了自动注册与手动注册。
如果使用自动注册,可以用org.postgis.DriverWrapper作为driver,然后jdbc的url使用jdbc:postgresql_postGIS
就可以自动注册org.postgis.Geometry。但是,源码中支持的数据库类型是geometry而不支持geography,这两者的
差别可以参考不睡觉的怪叔叔和德哥的文章
PostGIS教程十三:地理 PostGIS距离计算建议 - 投影与球坐标系, geometry与geography类型
为了让geography也能自动注册,项目中自定义了com.github.lonelyleaf.gis.db.DriverWrapper类,
转换出来还是org.postgis.Geometry,在java代码层使用和geometry并没做区分。
pgconn.addDataType("geography", org.postgis.PGgeometry.class);
pgconn.addDataType("public.geography", org.postgis.PGgeometry.class);
pgconn.addDataType("\"public\".\"geography\"", org.postgis.PGgeometry.class);
如果你没有注册类型,那么你拿到的类型会是org.postgresql.util.PGobject。但其实通过
org.postgis.PGgeometry#geomFromString()还是很可以获取PGobject#value来手动转换的。
4.3 mybatis中需要定义TypeHandler
明显mybatis没有原生支持org.postgis.Geometry与其各个子类。有个
mybatis-typehandlers-postgis
做了一些工作,但实在太简单我选择直接copy其中核心代码
public abstract class AbstractGeometryTypeHandler<T extends Geometry> extends BaseTypeHandler<T> {
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
PGgeometry geometry = new PGgeometry();
geometry.setGeometry(parameter);
ps.setObject(i, geometry);
}
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
PGgeometry pGgeometry = (PGgeometry) rs.getObject(columnName);
if (pGgeometry == null) {
return null;
}
return (T) pGgeometry.getGeometry();
}
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
PGgeometry pGgeometry = (PGgeometry) rs.getObject(columnIndex);
if (pGgeometry == null) {
return null;
}
return (T) pGgeometry.getGeometry();
}
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
PGgeometry pGgeometry = (PGgeometry) cs.getObject(columnIndex);
if (pGgeometry == null) {
return null;
}
return (T) pGgeometry.getGeometry();
}
}
@MappedTypes(Point.class)
public class PointTypeHandler extends AbstractGeometryTypeHandler<Point> {
}
注意如果你没有在jdbc connection中注册类型,那rs.getObject拿到的会是org.postgresql.util.PGobject。
4.4 org.postgis.Geometry的json序列化
由于没有现成的org.postgis.Geometry转为GeoJson的库,所以项目中使用了自定义SimplePoint类来
转换一下然后序列化。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SimplePoint {
private double x;
private double y;
public SimplePoint(org.postgis.Point point) {
this.x = point.x;
this.y = point.y;
}
}
其实postgis-jdbc中有org.postgis.jts包,对JTS有支持,JTS是有jackson库转GeoJson的。
如果需要在java层做些地理位置的运算,使用org.postgis.jts包应该更好。
4.5 坐标的保存与SRID
SRID的选择其实很复杂,详细解释可以参考下不睡觉的怪叔叔的文章https://blog.csdn.net/qq_35732147/article/details/86301242。
这里摘抄一段
地球不是平的,也没有简单的方法把它放在一张平面纸地图上(或电脑屏幕上),所以人们想出了各种巧妙的解决方案(投影)。
每种投影方案都有优点和缺点,一些投影保留面积特征;一些投影保留角度特征,如墨卡托投影(Mercator);
一些投影试图找到一个很好的中间混合状态,在几个参数上只有很小的失真。所有投影的共同之处在于,
它们将(地球)转换为平面笛卡尔坐标系,选择哪种投影取决于你将如何使用数据(需要哪些数据特征,面积?角度?或者其他)。
SRID其实就决定了你的坐标使用的哪种投影,由于我的数据都是标准的gps坐标(经纬度,没有偏移),
所以在转换与建表时,都使用了srid=4326。具体数据库应该使用哪种一定要根据业务来,不然使用postgis进行计算与使用
各种gis软件进行分析时一定会出问题。
在转换到org.postgis.Point时,项目中写死了srid的值,这不是必须,但为了保证正确最好这样做:
default org.postgis.Point toGisPoint(SimplePoint point) {
Point gisPoint = new Point();
gisPoint.x = point.getX();
gisPoint.y = point.getY();
gisPoint.dimension = 2;
//WGS84坐标系,也就是GPS使用的坐标
gisPoint.srid = 4326;
return gisPoint;
}
5. 总结
java与postgis交互其实不难,这里是用实体<->表对于的方法在建模。其实实在搞不懂,最次还可以全部靠 xml里手写sql搞定不是🐒
文章只介绍了数据的交互,但postgis的各种强大的空间分析的函数并没有介绍,等业务实践再积累些也许可以 再写一下。
如果对分析有兴趣,再次推荐下大佬不睡觉的怪叔叔的专栏