点线面的智慧: 转转JTS技术如何塑造上门履约地理布局

学术   2024-07-11 18:40   广东  


  • 1 引言

  • 2 JTS 介绍

    • 2.1 引入 jar 包

    • 2.2 基本的几何模型

    • 2.3 几何模型的描述格式

    • 2.4 空间关系

    • 2.5 空间操作

  • 3 快速判断是否支持上门

    • 3.1 最小外接矩形(MBR)

    • 3.2 空间索引

    • 3.3 整体方案流程

  • 4 几何图形的修复处理

  • 5 总结

  • 6 参考


1 引言

如上图所示,在转转上门履约的场景中,上门服务的覆盖区域是在地图上画电子围栏来划定的。这就涉及到一些几何图形的操作和空间关系判断,其中最核心问题就是要解决如何判断位置是否在上门覆盖范围内。下面介绍下 JTS,以及如何通过 JTS 的空间之力来解决这些问题。

2 JTS 介绍

JTS,全称 Java Topology Suite,是一个用于创建和操作向量几何的 Java 库。提供了对几何模型的抽象,以及各种空间操作和空间关系判断,非常强大。

2.1 引入 jar 包

JTS 有多个模块,这里只使用了核心的模块。

  • jts-core:提供几何模型的抽象、空间操作、空间关系判断算法等
  • jts-io-common:提供各种格式描述几何模型的输入输出包,如对 WKT、WKB 等格式
<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 的基本语法格式如下:

几何模型类型 (模型数据)

示例如下所示:

点:POINT (282 455)
线:LINESTRING (260 250, 485 248, 520 380)
多边形:POLYGON ((320 390, 370 330, 470 360, 460 430, 375 432, 320 390))

JTS 支持对该格式的读写操作,主要是两个对象WKTReaderWKTWriter,代码示例如下:

// 读取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

以该图形为例,两个多边形的关系判断的代码示例

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

在实际场景中,判断上门位置是否在上门区域内,转换成空间关系的判断就是点是否在多边形内。解决该问题的实例代码如下:

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)从一个几何对象中减去另一个几何对象

以该图为例,操作示例代码如下:

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 中?比较坐标值的大小就可以了。示例代码如下:

mbr.getLngMin() <= point.getLng()
&& mbr.getLngMax() >= point.getLng()
&& mbr.getLatMin() <= point.getLat()
&& mbr.getLatMax() >= point.getLat()

综上,MBR 用简单的矩形来近似表示复杂的几何形状,将复杂的空间关系简化为矩形之间的关系。 通过 MBR 这一层的初步筛选,就能够快速排除不可能有关系的多边形对象。

在 JTS 中,Envelope 对象来表示 MBR。代码示例如下:

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):将空间划分为规则的网格单元

空间索引的基本原理基本类似,采用分割原理,逐级划分地理空间。举个不那么恰当的例子,一个自上而下、逐级划分地理空间的索引定位过程如下:

北方 还是 南方 ? 南方
广东 还是 广西 ? 广东
深圳 还是 广州 ? 深圳
福田 还是 南山 ? 福田

JTS 提供了四叉树和 R 树的实现

  • Quadtree(四叉树)
  • STRtree(基于 R 树的变体)

以这个图形为例,使用 JTS 构建 R 树空间索引

示例代码如下:

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)在哪些多边形中的具体流程如下:

  1. 先通过 STRtree 构建空间索引
  2. 利用空间索引快速筛选可能包含点的多边形
  3. 对筛选后的多边形进行精确的空间关系判断

多边形是随时都有可能可以调整,如果一个多边形发生了调整就需要重构整颗索引树。但是在实践中,为了降低构建索引树的频次,通过定时任务去间隔 10 分钟在内存中构建一次。并且为了减少索引树占用的内存大小,向索引树中添加 MBR 关联的是多边形的 Id,初筛后再根据 id 从缓存中取具体的多边形数据进行精确的空间关系判断,实现一个类似懒加载的过程。

具体流程如下图所示:

4 几何图形的修复处理

在实际运营过程中,画的图形各种形状,会出现不少异常的情况,如点重叠、边之间细微的间隙、自交等问题。实际操作中还提拱了图形合并的能力,合并出来的图像也有可能也是不符合规范的。为此,需要对这些异常的图像进行修复。

常见的修复手段有两种

  • Buffer 操作:在几何对象周围的创建缓冲区,一般用来修复自相交问题、精度导致的小间隙等
  • Snap 操作:一个几何对象的顶点捕捉到另一个几何对象的顶点或边缘,一般用来修复小的拓扑错误

这两种操作也不是万能,也是需要自己根据实际情况进行不断地调整。

下面来看一个修复自交的例子,一个自交的图形如下所示:

修复代码示例如下:


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 参考

  • Java Topology Suite (JTS):https://github.com/locationtech/jts
  • OSGeo中国:https://www.osgeo.cn/

关于作者

揭荣,转转上门履约业务研发工程师

想了解更多转转公司的业务实践,欢迎点击关注下方公众号:



转转技术
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 各种干货实践,欢迎交流分享,如有问题可随时联系 waterystone ~
 最新文章