JTS (Java Topology Suite) 开发教程

JTS 简介

JTS (Java Topology Suite) 是一个开源 Java 软件库,它为欧几里得平面线性几何提供了一套对象模型以及一组基本的几何函数。JTS 主要作为矢量 GIS 软件的核心组件,也可以作为提供几何算法的通用库使用。

常用的 JTS 实现框架有 vividsolutions 以及 locationtech 两种,本文内容基于 locationtech 库。

目前,vividsolutions 核心库已经迁移到了 locationtech。

模块总览

Java Topology Suite 核心类库由以下模块构成:

  • algorithm:封装了一些几何属性的计算算法,例如计算几何体面积、线段长度等;
  • awt:基于 Java AWT 的图形绘制工具,不常用;
  • densify:内部填充器,用于将几何体致密化;
  • dissolve:用于将几何体中的线性分量融合成一组最大长度的 Linestring;
  • edgegraph:用于描述几何体边缘
  • geom:用于描述简单几何体
  • geomgraph:用于描述地理信息
  • index:索引(数据结构相关)
  • io:用于读取并解析 WKT/WKB、KML 以及 GML 语言;
  • linearref:线性参考系;
  • math:空间计算数学工具包;
  • noding:用于计算几何体的交点;
  • operation:用于操作几何图形,例如获取几何体的 buffer、计算几何体之间的距离等;
  • planargraph:用于描述平面图形;
  • precision:用于设定精确度;
  • shape:用于快速生成一些几何模型,例如生成随机点、生成莫顿曲线等;
  • simplify:几何体简化工具;
  • triangulate:用于处理三角形相关内容;
  • util:一些工具类。

Geometry

Coordinate(重要)

Coordinate 用于存储几何点的坐标,它虽然不在 Geometry 继承体系内,但却是描述几何体的基础Geometry 本质就是一群有意义的点集,而每个点的空间位置都需要通过 Coordinate 来表示。这里我们重点关注其以下几个特性:

  1. Coordinate 内部通过 double x, y, z 三个字段描述一个位置的坐标,其中 xy 表示平面坐标,z 表示高程;
  2. Coordinate 的某些实现类支持自定义度量 (measure),并在内部通过 double m 字段表示;
  3. Coordinate 内部通过 static final int X, Y, Z, M 四个常量描述 x、y、z、m 之间的顺序。例如某个坐标只支持二维平面,且支持自定义度量,则这四个常量的值为 X = 0, Y = 1, Z = -1, M = 2

为此,JTS 提供了以下三个实现类来描述不同的坐标类型:

  • CoordinateXY:用于表示二维平面的坐标,且不支持自定义度量。
  • CoordinateXYM:用于表示二维平面的坐标,且支持自定义度量。
  • CoordinateXYZM:用于表示三维空间的坐标系,且支持自定义度量。

几何类型

介绍完基础的坐标概念,我们再来看 Geometry 体系结构下的几何类型。JTS 在 jts.geom 包中提供了以下几种几何类型:

类型描述
Geometry表示抽象几何类型,是下列所有几何的基类
Point表示“点”,内部通过 Coordinate 表示空间位置
MultiPoint表示“多个点”,继承自 GeometryCollection,内部通过 Geometry[] 数组保存 Point 序列
LineString表示“线段”,内部维护了一个坐标序列 (CoordinateSequence)
MultiLineString表示“多条线段”,也继承自 GeometryCollection,并通过 Geometry[] 数组保存 LineString 序列
LinearRing继承自 LineString,表示首尾相连,形成了环的 LineString
Polygon表示“多边形”,由外壳 (Shell) 与内部的孔洞 (hole) 组成
MultiPolygon表示“多个多边形”,也继承自 GeometryCollection
GeometryCollection表示几何体的集合,通过 Geometry[] 数组可以保存各种类型的几何对象

其架构如下:

JTS 架构
JTS 架构

创建几何对象

JTS 通过 GeometryFactory 工厂创建几何对象:

1
GeometryFactory geometryFactory = new GeometryFactory();

下面我们演示创建各类几何对象的方法。

创建 Point、MultiPoint

  1. 通过坐标(Coordinate)创建

    1
    2
    3
    4
    5
    
    Coordinate coordinate = new Coordinate(12.654210, 69.332525);
    Point point = geometryFactory.createPoint(coordinate);
    Coordinate coordinate1 = new Coordinate(13.654210, 70.332525);
    Point point1 = geometryFactory.createPoint(coordinate1);
    MultiPoint multiPoint = geometryFactory.createMultiPoint(new Point[]{point,point1});
  2. 通过 WKT 创建

    1
    2
    3
    4
    5
    
    WKTReader wktReader = new WKTReader(geometryFactory);
    Point point = (Point) wktReader.read("POINT (12.654210, 69.332525)");
    
    MultiPoint multiPoint = (MultiPoint) wktReader.read("MULTIPOINT (12.654210 69.332525,
    13.654210 70.332525)");

创建 LineString、MultiLineString

  1. 通过坐标创建

    1
    2
    3
    4
    5
    6
    7
    
    Coordinate[] coords  = new Coordinate[] {new Coordinate(2, 2), new Coordinate(3, 3)};
    LineString lineString = geometryFactory.createLineString(coords);
    Coordinate[] coords1  = new Coordinate[] {new Coordinate(3, 3), new Coordinate(4, 4)};
    LineString lineString1 = geometryFactory.createLineString(coords1);
    
    LineString[] lineStrings = new LineString[]{lineString, lineString1};
    MultiLineString multiLineString = geometryFactory.createMultiLineString(lineStrings);
  2. 通过 WKT 创建

    1
    2
    3
    
    LineString lineString = (LineString) reader.read("LINESTRING(0 0, 2 0)");
    MultiLineString multiLineString = (MultiLineString) reader.read("MULTILINESTRING((0 0, 2 0),
    (1 1,2 2))");

创建闭合线 LinearRing

1
2
3
4
5
6
7
8
9
LinearRing linearRing = geometryFactory.createLinearRing(
  new Coordinate[]{
    new Coordinate(0, 0), 
    new Coordinate(0, 10), 
    new Coordinate(10, 10), 
    new Coordinate(10, 0), 
    new Coordinate(0, 0)
  }
);

创建 Polygon、MultiPolygon

多边形由一个外壳 (shell) 和内部的一些孔洞 (hole) 构成

  1. 通过坐标创建

    1
    2
    3
    4
    5
    6
    7
    
    LinearRing shell = new GeometryFactory().createLinearRing(..);
    LinearRing hole = new GeometryFactory().createLinearRing(..);
    LinearRing hole1 = new GeometryFactory().createLinearRing(..);
    Polygon polygon = geometryFactory.createPolygon(shell, 
        new LinearRing[]{hole, hole1});
    
    MultiPolygon mp = geometryFactory.createMultiPolygon(new Polygon[]{polygon, ...});
  2. 通过 WKT 创建

    1
    2
    3
    4
    5
    
    WKTReader reader = new WKTReader(geometryFactory);
    Polygon polygon = (Polygon) reader.read("POLYGON((20 10, 30 0, 40 10, 30 20, 20 10))");
    
    MultiPolygon mpolygon = (MultiPolygon) reader.read("MULTIPOLYGON(((40 10, 30 0, 40 10, 30 20, 40 10),
    (30 10, 30 0, 40 10, 30 20, 30 10)))");

创建 GeometryCollection

1
2
Geometry[] garray = new Geometry[]{geometry1, geometry2, ...};
GeometryCollection gc = geometryFactory.createGeometryCollection(garray);

空间关系判断

DE-9IM 模型

DE-9IM 全称 Dimensionally Extended nine-Intersection Model,是一种拓扑模型,是描述两个几何图形空间关系的一种标准。在专业领域,通常将每个几何图形分为三部分:外部,边界和内部。

两个图形的关系判断,实际上就是对这三个部分的分别判断,因此就会有一个 3*3 交叉矩阵,这个矩阵就是 DE-9IM 模型,如下表:

de-9im 模型

以上 DE-9IM 矩阵中:

  • a^0 表示 a 内部;∂a 表示 a 边界;a^e 表示 a 外部。
  • dim() 表示相交部分的维度。
    • 如果相交部分是面,则 dim = 2;
    • 如果相交部分是线,则 dim = 1;
    • 如果相交部分为一些点,则 dim = 0;
    • 如果不相交,则 dim = -1;

接下来我们找两个平面举例:

de-9im 示例

这两个几何体的 DE-9IM 矩阵如下:

de-9im 矩阵

以从左往右,从上往下的顺序读取这个矩阵,便可以用一个字符串来表示:212101212

空间谓词

任何基于 DE-9IM 二进制空间关系的拓扑属性都是空间谓词,以下是一些工作中最常用的空间谓词:

注意: 在能够忽略相交维度的情况下(无需探讨是什么类型相交的场景),空间谓词可以用布尔值来代替相交的维度:有相交则为 T(rue),否则为 F(alse)。

命名空间谓词

空间关系分析

缓冲区分析 (buffer)

含义:包含所有的点在一个指定距离内的多边形或多多边形。示意图如下:

jts buffer

API 详解:

1
public Geometry buffer(double distance);
  • distance 参数:缓冲区扩展的距离(可以是正数、负数或 0):
    • 大于 0:向外扩张,示意图:

      buffer 外扩 10 单位
    • 等于 0:buffer 即是边界,示意图:

      buffer 外扩 0 单位
    • 小于 0:向内缩,如果内缩后 buffer 的面积小于等于 0,则返回 POLYGON EMPTY,示意图:

      buffer 内扩 20 单位
1
public Geometry buffer(double distance, int quadrantSegments);
  • quadrantSegments:控制缓冲区突出位置的圆角精度
    • quadrantSegments >= 1: 手动控制逼近弧度的精度,值越大弧越精确,以下是 quadrantSegments = 2 的示意图:

      quadrantSegments >= 1
    • quadrantSegments < 1:JTS 自己控制逼近程度

1
public Geometry buffer(double distance, int quadrantSegments, int endCapStyle);
  • endCapStyle:端点处 buffer 形状,只对 LineString 有效
    • BufferParameters.CAP_ROUND:端点处缓冲区为圆的一部分

      CAP_ROUND
    • BufferParameters.CAP_FLAT:端点处的缓冲区边界直接穿过端点切下来

      CAP_FLAT
    • BufferParameters.CAP_SQUARE:端点处向外延伸出一个长方形区域

      CAP_SQUARE

除了常规的 buffer,JTS 还支持用于 LineStringSideBuffer 等,道理与普通 buffer 一样,就不展开了。

交叉分析 (Intersection)

含义:获取多边形交叉部分的共同点构成的集合。示意图如下:

jts intersection

API 详解:

1
public Geometry intersection(Geometry other);

geometryA.intersection(geometryB):获取两个几何体之间的交叉部分的几何体。

凸壳分析 (ConvexHull)

含义:包含几何形体的所有点的最小凸壳多边形(外包多边形)。示意图如下:

jts convexhull

API 详解:

1
public Geometry convexHull();

获取几何体的凸壳

联合分析 (Union)

含义:获取多面体所有点的集合。示意图如下:

jts Union

API 详解:

1
public Geometry union(Geometry other);

geometryA.union(geometryB):获取两个几何体之间的联合部分。

差异分析 (Difference)

含义:获取一个多面体里有,另一个多面体里没有的点的集合。示意图如下:

jts-difference

API 详解:

1
public Geometry difference(Geometry other);

geometryA.difference(geometryB):获取两个几何体之间的差异部分。

对称差异分析 (SymDifference)

含义:不同时在两个多面体中的所有点的集合。示意图如下:

jts-sym-difference

API 详解:

1
public Geometry symDifference(Geometry other);

geometryA.symDifference(geometryB):获取不同时相交与两个几何体的部分。

Envelope (矩形)

Envelope 用于表示几何体的矩形外框,它通过 double minx, maxx, miny, maxy 这四个属性值描述几何体在二维平面上的边界,并由这四条边界构成一个矩形。示意图如下:

1
2
3
4
5
6
7
8
9
y
^
|maxy +-----------+ 
|     |    _|——   |
|     |   /     \ |
|     |=‘.______-||
|miny +-----------+
|    minx        maxx
+------------------------------> x

Envelope 主要用于性能优化和空间索引,以下是对各个使用场景的详细介绍:

  1. 性能优化:当我们判断两个几何体是否相交时,JTS 并不会直接计算它们的 DE-9IM 矩阵,而是先通过 Envelope 反向计算是否不相交,如果不相交就直接跳过了,从而实现加速。
  2. 空间索引:为了简化算法实现,大部分的空间索引的 Key 都是基于矩形构建的,关于空间索引我们下文再详细介绍。

空间索引 SpatialIndex 与 STRTree

在 jts.index 包中存在许多空间索引工具,这些工具在日常开发中至关重要。例如,我们经常需要查询 Geo Buffer 周围是否存在其他要素。

索引操作的顶层接口是 SpatialIndex,其 API 如下:

方法声明描述
insertvoid insert(Envelope itemEnv, Object item)将 item 插入索引,建立 item 与 itemEnv 矩形的映射关系
queryList query(Envelope searchEnv)根据矩形范围,查询与该矩形相交的所有 Obj
queryvoid query(Envelope searchEnv, ItemVisitor visitor)根据矩形范围查询,并将结果封装为 Visitor
removeboolean remove(Envelope itemEnv, Object item)移除矩形范围内的 item 对象

至于其实现原理,我将留到数据结构与算法专栏里详细介绍,这里我们先通过工作中最常用的 STRTree,对空间索引有个大致的映像:STRTree 是一种基于 R 树的空间索引结构,它将空间中的几何体划分为小的矩形区域(称为"瓦片"),并通过递归构建出树状数组来管理这些瓦片。在系统初始化阶段,我们需要将地图数据中的所有信息插入这棵树中,以建立空间与要素之间的关联关系。示意图如下:

STRTree
  • 蓝、橙、红框表示空间索引 Key;
  • 黑色不规则图形表示与 Key 关联的地图要素。

构建完毕后,即可通过几何体的矩形边框查询与之关联的要素了。不过有一点需要注意,索引查出来的数据量集中可能还包含我们不需要的数据,实际业务中还需要通过 空间关系判断 做进一步过滤。

构建不规则三角网 (TIN)

除了前文介绍的点、线、面等基本几何类型,GIS 系统中还广泛使用一种特殊的几何类型,即不规则三角网 (TIN, Triangulated Irregular Network)。

TIN 是在 GIS 系统和计算机图形领域中广泛使用的一种地形建模空间数据表示方法。其基本思想是将地形表面分割为许多不规则的三角形,每个三角形由地面上的三个离散点定义。示意图如下:

tin

JTS 可以通过可以通过点集WKT 来创建 TIN 几何对象,相关实现位于 jts.triangulate 包中。

通过点集构建 TIN

首先我们创建四个坐标点,分别是 (1,1)、(1,2)、(2,2)、(2,1),这四个点将会构成一个正方形:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/*   ^
 * 2 |      .--------.
 *   |      |        | 四个点构成一个正方形
 *   |      |        |
 * 1 |      `--------'
 *   |
 * 0 +----------------------------->
 *   0      1        2
 */
GeometryFactory geometryFactory = new GeometryFactory();
Coordinate[] coordinates = new Coordinate[] {
        new Coordinate(1, 1),
        new Coordinate(2, 1),
        new Coordinate(2, 2),
        new Coordinate(1, 2),
        // 当然,这里可以追加更多坐标点
};

然后通过 DelaunayTriangulationBuilder 创建 TIN:

1
2
3
4
5
6
// 获取 builder
DelaunayTriangulationBuilder dtb = new DelaunayTriangulationBuilder();
// 拿到点阵集合 coordinates,并注册到 builder 内
dtb.setSites(Arrays.asList(coordinates));
// 然后获取 tin 集合体
Geometry tinGeometry = dtb.getTriangles(geometryFactory);

最终得到的 TIN 几何体为 GEOMETRYCOLLECTION(POLYGON((1 2,1 1,2 1,1 2)),POLYGON((1 2,2 1,2 2,1 2))),其形状如下:

tinGeometry

可见,JTS 将这个平行四边形从中间划分开,形成了由两个三角形组成的 TIN。

通过 WKT 构建 TIN

通过 WKT 创建 TIN 也很简单,还是以这四个点为例:

1
2
3
4
5
6
7
8
9
GeometryFactory geometryFactory = new GeometryFactory();
DelaunayTriangulationBuilder dtb = new DelaunayTriangulationBuilder();
WKTReader wktReader = new WKTReader(geometryFactory);
// 创建这四个点的 Geometry
Geometry geometry = wktReader.read("POLYGON((1 1, 1 2, 2 2, 2 1, 1 1))");
// 注册
dtb.setSites(geometry);
// 获取 tin
Geometry tinGeometry = dtb.getTriangles(geometryFactory);

首先通过 WKT 创建点集的 Geometry,然后调用 setSites 的重载方法完成注册,之后的流程就与之前一样了。

几何体致密化 (Densify)

在日常工作中,经常会遇到需要进行垂线或投影操作的情况。然而,在 JTS 中进行投影操作时,本质上是计算被投影体到目标体上最近点的距离。如果目标体上的点比较稀疏,就会产生较大的误差。为了提高投影的准确性,我们可以使用 Densify 对几何体进行致密化处理,以提升计算精度。

API 详解:

1
Densifer.densify(Geometry geom, double distanceTolerance);
  • geom:待处理的几何体;
  • distanceTolerance:致密化后的点距,单位为弧度。

示意图如下:

jts-densifer

扩展:WKT 与 WKB 标记语言

WKT

Well-known text (WKT) 是一种用于表示矢量几何对象的文本标记语言,其表示的几何对象有:点、线、多边形、TIN 和多面体。下面是一些例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))
POINT ZM (1 1 5 60)
POINT M (1 1 80)
POINT EMPTY
MULTIPOLYGON EMPTY
TRIANGLE((0 0 0,0 1 0,1 1 0,0 0 0))
TIN (((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0)))
POLYHEDRALSURFACE Z ( PATCHES
    ((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0)),
    ((0 0 0, 0 1 0, 0 1 1, 0 0 1, 0 0 0)),
    ((0 0 0, 1 0 0, 1 0 1, 0 0 1, 0 0 0)),
    ((1 1 1, 1 0 1, 0 0 1, 0 1 1, 1 1 1)),
    ((1 1 1, 1 0 1, 1 0 0, 1 1 0, 1 1 1)),
    ((1 1 1, 1 1 0, 0 1 0, 0 1 1, 1 1 1))
)
  • 几何坐标可以是 2D (x, y)、3D (x, y, z)、4D (x, y, z, m),其中 m 表示度量。
  • 或者是 2D + m 值 (x, y, m)。
  • 也可以通过在类型名称后使用符号 EMPTY 来指定不包含坐标的空几何。

WKB

Well-known binary (WKB) 是 WKT 的二进制等效物,用于以更紧凑的形式传输和存储相同的信息。WKB 由 1 字节无符号整数 + 4 字节无符号整数 + 8 字节双精度数字组成:

  • 第一个字节表示字节顺序:
    1. 00:大端模式
    2. 01:小端模式
  • 接下来的四个字节表示几何类型:
    类型2DZMZM
    Geometry0000100020003000
    Point0001100120013001
    LineString0002100220023002
    Polygon0003100320033003
    MultiPoint0004100420043004
    MultiLineString0005100520053005
    MultiPolygon0006100620063006
    GeometryCollection0007100720073007
    CircularString0008100820083008
    CompoundCurve0009100920093009
    CurvePolygon0010101020103010
    MultiCurve0011101120113011
    MultiSurface0012101220123012
    Curve0013101320133013
    Surface0014101420143014
    PolyhedralSurface0015101520153015
    TIN0016101620163016
    Triangle0017101720173017
    Circle0018101820183018
    GeodesicString0019101920193019
    EllipticalCurve0020102020203020
    NurbsCurve0021102120213021
    Clothoid0022102220223022
    SpiralCurve0023102320233023
    CompoundSurface0024102420243024
    BrepSolid1025
    AffinePlacement1021102
  • 下面以存储 POINT(2.0 4.0) 为例: 00 00000001 4000000000000000 4010000000000000
    • 00 :表示大端模式
    • 00000001 :表示 Point 2D (x,y) 类型数据
    • 4000000000000000 :64 位双精度浮点数,表示 x 坐标
    • 4010000000000000 :64 位双精度浮点数,表示 y 坐标

写在最后

之所以写这篇文章,是因为最近接手了国内某地图公司的相关项目,需要用到 JTS 相关的内容。而这块内容对于普通的开发者来说确实比较冷门,我刚被抽调过来时也是一脸懵逼,可以预见以后还会有更多的同事也会遇到这个麻烦。不过,只要你仔细将本文学完,入门就不成问题了,一旦入了门,剩下的路就会变得轻松许多,加油!

0%