现在很多应用都需要根据距离对信息进行排序,MYSQL5.7.28以后新增了空间数据类型(Spatial Data Type 即geometry)和相应的距离计算。
关于如何进行距离计算和排序不是本文要说的重点。本文要说明的是如何将MySQL存储的空间数据类型(Spatial Data Type)转为对应的Java对象以方便访问。
地理空间数据格式是有规范的,OpenGIS即(Open Geodata Interoperation Specification,OGIS-开放的地理数据互操作规范)由美国OGC(OpenGIS协会,Open Geospatial Consortium)提出。OGC是一个非盈利性组织,目的是促进采用新的技术和商业方式来提高地理信息处理的互操作性(Interoperability),它致力于消除地理信息应用(如地理信息系统,遥感,土地信息系统,自动制图/设施管理(AM/FM)系统)之间以及地理应用与其它信息技术应用之间的藩篱,建立一个无“边界”的、分布的、基于构件的地理数据互操作环境。
也就是说OpenGIS对地理空间数据做了规范定义,MySQL定义的空间数据类型(比如POINT–点,POLYGON–多边形)在存储时也都遵循OpenGIS的数据规范。参见MySQL官方文档《11.4 Spatial Data Types》
同时OpenGIS还提供了几何基础类库实现这些定义,对应到Java语言的就是JTS库《JTS Topology Suite》
根据MySQL官方文档《11.4 Spatial Data Types》说明,MySQL是以二进制形式存储存储空间数据类型(即WKB),比如对于一个POINT(1 -1)
,就是保存为长度25个字节的数组。格式如下:
Component | Size | Value |
---|---|---|
SRID | 4 byte | 0 |
Byte order | 1 byte | 01 |
WKB type | 4 bytes | 01000000 |
X coordinate | 8 bytes | 000000000000F03F |
Y coordinate | 8 bytes | 000000000000F0BF |
上面的格式很简单,如果自己对这些数据进行解析也不是很难的事儿,但这样要自己定义相关的类,写好多代码。
事实上JTS库已经帮我们实现了实现了二进制数据到Java数据对象(Geometry)的相互转换。我们没有必要重复造轮子。
我们所要做的就是代码中使用JTS库的Geometry对象保存空间数据,并通过JTS库来实现将Geometry序列化为WKB数据保存到数据库,以及将从数据库中读取的WKB格式二进制数据反序列化为Geometry对象。
关于WKB,WKT格式的说明参见本文最后《参考资料》一节提供的链接
JTS库依赖引入
<dependency>
<groupId>com.vividsolutions</groupId>
<artifactId>jts</artifactId>
<version>1.13</version>
</dependency>
WKB解析
以下代码实现MySQL的WKB数据解析为com.vividsolutions.jts.geom.Geometry
类的过程,
/**
* 将MySQL存储的WKB格式的二进制数据解析为{@link Geometry}对象
* @param binary
* @throws ParseException
*/
public Geometry fromWKB(byte[] binary) throws ParseException {
if(null == binary) {
return null;
}
if(binary.length < 25) {
throw new ParseException("INVALID binary data length,more than 25 bytes required");
}
int srid = ByteBuffer.wrap(binary,0,4).asIntBuffer().get();
WKBReader wkbReader = new WKBReader();
Geometry geo = wkbReader.read(Arrays.copyOfRange(binary, 4, binary.length));
geo.setSRID(srid);
return geo;
}
public final <T extends Geometry>T fromWKB(byte[] binary, Class<T> targetType) throws ParseException {
return targetType.cast(fromWKB(binary));
}
WKBReader在解析WIKB数据时并不会处理最开始的SRID部分,所以上面的代码中会先从数组头部读取4个字节作为SRID,将数组索引4开始的剩余数据交给WKBReader解析。
Geomerty序列化为WKB
/**
* 将{@link Geometry}类型转为适合MySQL数据库存储的二进制格式
* @param <T>
* @param input
*/
public <T extends Geometry>byte[] toWKB(T input) {
if(null == input) {
return null;
}
WKBWriter wkbWriter = new WKBWriter(2, ByteOrderValues.LITTLE_ENDIAN);
byte[] binary = wkbWriter.write(input);
byte[] out = new byte[4 + binary.length];
/** 写入SRID */
ByteBuffer.wrap(out).asIntBuffer().put(input.getSRID());
System.arraycopy(binary, 0, out, 4, binary.length);
return out;
}
根据MySQL的官方说明,WKB存储时字节序(Byte order)为小端(little-endian),而WKBWriter的默认构造方法创建对象时默认字节序为大端,所以这里不可以使用默认构造方法创建对象,必须指定为小端。
ResultSet
从数据查询结果集对象(ResultSet)中读取Geometry对象.
/**
* 读取数据记录指定字段的值转为空间数据对象
* @param rs
* @param columnIndex
* @throws SQLException
*/
public final Object readGeometryData(ResultSet rs, int columnIndex) throws SQLException {
try {
return fromWKB(checkNotNull(rs,"rs is null").getObject(columnIndex));
} catch (ParseException e) {
throw new SQLException(e);
}
}
调用示例
@Test
public void test1() {
try {
Point point = new GeometryFactory().createPoint(new Coordinate(10,20));
System.out.printf("point %s\n",point);
byte[] binary = toWKB(point);
Geometry p2 = fromWKB(binary);
System.out.printf("p2 %s\n",p2);
} catch (Exception e) {
log(e.getMessage(),e);
}
}
完整代码参见我的码云仓库:https://gitee.com/l0km/sql2java/blob/dev/sql2java-base/src/main/java/gu/sql2java/geometry/MysqlGeometryDataCodec.java