1 引言
如上图所示,在转转上门履约的场景中,上门服务的覆盖区域是在地图上画电子围栏来划定的。这就涉及到一些几何图形的操作和空间关系判断,其中最核心问题就是要解决如何判断位置是否在上门覆盖范围内。下面介绍下 JTS,以及如何通过 JTS 的空间之力来解决这些问题。
2 JTS 介绍
JTS,全称 Java Topology Suite,是一个用于创建和操作向量几何的 Java 库。提供了对几何模型的抽象,以及各种空间操作和空间关系判断,非常强大。
2.1 引入 jar 包
JTS 有多个模块,这里只使用了核心的模块。
- jts-core:提供几何模型的抽象、空间操作、空间关系判断算法等
- jts-io-common:提供各种格式描述几何模型的输入输出包,如对 WKT、WKB 等格式
xml
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>org.locationtech.jts.io</groupId>
<artifactId>jts-io-common</artifactId>
<version>1.19.0</version>
</dependency>
2.2 基本的几何模型
JTS 提供了常见的几何模型抽象,并且各具特点。
模型 | 定义 | 常见应用 |
---|---|---|
点(Point) | 空间中的单个位置,由一对 x,y 坐标表示 | 兴趣点、事件位置等 |
多点(MultiPoint) | 由多个独立的点组成的几何对象 | 表示多个相关但分散的位置,如连锁店分布,多个不同人位置 |
线(LineString) | 由一系列点组成的一维几何对象,有起点和终点,中间可以有任意数量的点 | 表示道路、河流等线性特征 |
多线(MultiLineString) | 由多个不相连的 LineString 组成的几何对象 | 表示复杂的道路网络、等高线等 |
多边形(Polygon) | 由一系列首尾相连的线段围成的平面区域(可以有内部空洞) | 表示行政区划、建筑物轮廓等 |
多多边形(MultiPolygon) | 由多个独立的 Polygon 组成的几何对象,可以表示不相连的多个区域 | 表示群岛、复杂的行政区划 |
几何集合(GeometryCollection) | 可以包含任意类型几何对象的集合,最灵活的几何类型,可以混合包含点、线、面等 | 表示复杂的空间场景,如包含多种类型要素的地图 |
在 JTS 中的各几何模型对象关系如下所示
在实际应用场景中,最常使用的模型如下
- 点(Point):表示位置信息,如用户地址位置、工程师位置等
- 多边形(Polygon)、多多边形(MultiPolygon):用来表示上门履约的覆盖区域
2.3 几何模型的描述格式
WKT(Well-Know Text)格式是一种文本格式,用于描述二维和三维几何对象的空间特征。 WKT 的基本语法格式如下:
scss
几何模型类型 (模型数据)
示例如下所示
scss
点:POINT (282 455)
线:LINESTRING (260 250, 485 248, 520 380)
多边形:POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))
JTS 支持对该格式的读写操作,主要是两个对象WKTReader
和WKTWriter
,代码示例如下
java
// 读取wkt描述的几何对象
WKTReader wktReader = new WKTReader();
Geometry point = wktReader.read("POINT (282 455)");
Geometry line = wktReader.read("LINESTRING (260 250, 485 248, 520 380)");
Geometry polygon = wktReader.read("POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))");
// 输出几何对象的wkt描述
WKTWriter wktWriter = new WKTWriter();
System.out.println(wktWriter.write(point));
System.out.println(wktWriter.write(line));
System.out.println(wktWriter.write(polygon));
2.4 空间关系
JTS 中的空间关系是基于 DE-9IM(Dimensionally Extended Nine-Intersection Model)模型定义的,这里列举常见的空间关系
空间关系 | 定义 |
---|---|
相等 (Equals) | 两个几何对象在拓扑上相等 |
相离 (Disjoint) | 两个几何对象没有任何共同点 |
相交 (Intersects) | 两个几何对象有至少一个共同点 |
内含 (Within) | 几何对象 A 完全位于几何对象 B 内部 |
包含 (Contains) | 几何对象 A 完全包含几何对象 B |
以该图形为例,两个多边形的关系判断的代码示例
java
WKTReader wktReader = new WKTReader();
Geometry geometryA = wktReader.read("POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))");
Geometry geometryB = wktReader.read("POLYGON ((500 420, 430 360, 530 260, 500 420))");
System.out.println("Equal: " + geometryA.equals(geometryB));
System.out.println("Disjoint: " + geometryA.disjoint(geometryB));
System.out.println("Intersects: " + geometryA.intersects(geometryB));
System.out.println("Within: " + geometryA.within(geometryB));
System.out.println("Contains: " + geometryA.contains(geometryB));
在实际场景中,判断上门位置是否在上门区域内,转换成空间关系的判断就是点是否在多边形内。解决该问题的实例代码如下
java
WKTReader wktReader = new WKTReader();
Geometry geometryA = wktReader.read("POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))");
Geometry geometryB = wktReader.read("POLYGON ((500 420, 430 360, 530 260, 500 420))");
Geometry point = wktReader.read("POINT (390 380)");
System.out.println("point in geometryA: " + geometryA.contains(point));
System.out.println("point in geometryB: " + geometryB.contains(point));
2.5 空间操作
JTS 提供了丰富的空间操作功能,用于处理和分析几何对象。这里列举常见的几种
空间操作 | 定义 |
---|---|
相交 (Intersection) | 计算两个几何对象的共同部分 |
并集 (Union) | 合并两个或多个几何对象 |
差集 (Difference) | 从一个几何对象中减去另一个几何对象 |
以该图为例,操作示例代码如下
java
WKTReader wktReader = new WKTReader();
Geometry geometryA = wktReader.read("POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))");
Geometry geometryB = wktReader.read("POLYGON ((500 420, 430 360, 530 260, 500 420))");
System.out.println("Intersection: " + wktWriter.write(geometryA.intersection(geometryB)));
System.out.println("Union: " + wktWriter.write(geometryA.union(geometryB)));
System.out.println("Difference: " + wktWriter.write(geometryA.difference(geometryB)));
下面是 Union 合并后的效果
3 快速判断是否支持上门
在上门履约实际场景中,需要快速的识别用户所在位置、地址位置是否在上门服务的覆盖区域内。转换成空间关系的判断上,也就是点是否在多边形内(PIP,Point-In-Polygon)问题了。
在上述的 JTS 介绍中,已经得知 JTS 提供了 contains 的关系判断能力。但是这只是解决了单个问题,假设全国共有 N 个多边形,那么就需要遍历 N 个多边形来判断,复杂度是 O(N),并且还需要全部多边形加载到内存中。可想而知,直接使用的话会存在性能问题。为此,我们需要一个快速解决 PIP 问题的方案。
3.1 最小外接矩形(MBR)
最小外接矩形 MBR (Minimum Bounding Retangle),是能够完全包含一个几何对象的最小矩形。 如下图所示,这个规则的矩形就是该多边形的 MBR 表示。
表示 MBR 非常简单,只需要知道他的左下角和右上角,那么就可以知道这个 MBR 图形了。如下图所示
知道了这个最小外接矩形有什么用? 可以断定:如果点不在这个 MBR 内了,那么肯定不在这个多边形内。所以把点和 MBR 进行比较,就能够快速排除不可能有关系的多边形对象。
那么如何快速的判断点是否在 MBR 中?比较坐标值的大小就可以了。示例代码如下
java
mbr.getLngMin() <= point.getLng()
&& mbr.getLngMax() >= point.getLng()
&& mbr.getLatMin() <= point.getLat()
&& mbr.getLatMax() >= point.getLat()
综上,MBR 用简单的矩形来近似表示复杂的几何形状,将复杂的空间关系简化为矩形之间的关系。 通过 MBR 这一层的初步筛选,就能够快速排除不可能有关系的多边形对象。
在 JTS 中,Envelope 对象来表示 MBR。代码示例如下
java
WKTReader wktReader = new WKTReader();
Geometry geometryA = wktReader.read("POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))");
Envelope envelope = geometryA.getEnvelopeInternal();
System.out.println(envelope.getMaxX());
System.out.println(envelope.getMaxY());
System.out.println(envelope.getMinX());
System.out.println(envelope.getMinY());
3.2 空间索引
上述构建 MBR 可以理解为简单索引的一种,实际上有复杂的空间索引。常见空间索引有
- R 树(R-tree):平衡树,适用于多维空间数据(类似一维的 B+树)
- 四叉树(Quad-tree):将二维空间递归地分为四个象限
- 网格(Grid):将空间划分为规则的网格单元
空间索引的基本原理基本类似,采用分割原理,逐级划分地理空间。举个不那么恰当的例子,一个自上而下、逐级划分地理空间的索引定位过程如下
java
北方 还是 南方 ? 南方
广东 还是 广西 ? 广东
深圳 还是 广州 ? 深圳
福田 还是 南山 ? 福田
JTS 提供了四叉树和 R 树的实现
- Quadtree(四叉树)
- STRtree(基于 R 树的变体)
以这个图形为例,使用 JTS 构建 R 树空间索引
示例代码如下
java
WKTReader wktReader = new WKTReader();
Geometry geometryA = wktReader.read("POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))");
Geometry geometryB = wktReader.read("POLYGON ((500 420, 430 360, 530 260, 500 420))");
STRtree rtree = new STRtree();
// 向R树种添加MBR,和自己的数据
rtree.insert(geometryA.getEnvelopeInternal(), "Polygon-A");
rtree.insert(geometryB.getEnvelopeInternal(), "Polygon-B");
rtree.build();
// 点只在Polygon-A中
System.out.println(rtree.query(wktReader.read("POINT (337 391)").getEnvelopeInternal()));
// 点只在Polygon-B中
System.out.println(rtree.query(wktReader.read("POINT (496 390)").getEnvelopeInternal()));
// 点在Polygon-A和Polygon-B的交集中
System.out.println(rtree.query(wktReader.read("POINT (452 367)").getEnvelopeInternal()));
3.3 整体方案流程
综上所述,快速定位点(Point)在哪些多边形中的具体流程如下
- 先通过 STRtree 构建空间索引
- 利用空间索引快速筛选可能包含点的多边形
- 对筛选后的多边形进行精确的空间关系判断
多边形是随时都有可能可以调整,如果一个多边形发生了调整就需要重构整颗索引树。但是在实践中,为了降低构建索引树的频次,通过定时任务去间隔 10 分钟在内存中构建一次。并且为了减少索引树占用的内存大小,向索引树中添加 MBR 关联的是多边形的 Id,初筛后再根据 id 从缓存中取具体的多边形数据进行精确的空间关系判断,实现一个类似懒加载的过程。
具体流程如下图所示
4 几何图形的修复处理
在实际运营过程中,画的图形各种形状,会出现不少异常的情况,如点重叠、边之间细微的间隙、自交等问题。实际操作中还提拱了图形合并的能力,合并出来的图像也有可能也是不符合规范的。为此,需要对这些异常的图像进行修复。
常见的修复手段有两种
- Buffer 操作:在几何对象周围的创建缓冲区,一般用来修复自相交问题、精度导致的小间隙等
- Snap 操作:一个几何对象的顶点捕捉到另一个几何对象的顶点或边缘,一般用来修复小的拓扑错误
这两种操作也不是万能,也是需要自己根据实际情况进行不断地调整。
下面来看一个修复自交的例子,一个自交的图形如下所示
修复代码示例如下
java
WKTReader wktReader = new WKTReader();
Geometry geometryA = wktReader.read("POLYGON ((340 490, 370 330, 730 350, 700 270, 340 490))");
WKTWriter wktWriter = new WKTWriter();
wktWriter.setPrecisionModel(new PrecisionModel(0));
System.out.println(wktWriter.write(geometryA.buffer(0)));
修复之后如下图所示
5 总结
Java Topology Suite (JTS) 作为一个功能强大的空间数据处理库,为开发者提供了丰富的工具来处理复杂的空间问题。它在许多地理信息系统得到了广泛的应用。这里只是对其的一个简单应用,后续还待更深入的挖掘。
6 参考
关于作者
揭荣,转转上门履约业务研发工程师
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~