第02章 - 安装配置与快速开始

上一章讲清了 JTS 的定位,这一章开始动手。JTS 的安装简单到有些“反常识”:它没有安装程序、没有配置文件、没有服务端口,甚至没有必须初始化的运行时。你需要的只是一个 jar 包和一段能调用它的 Java 代码。

但“简单”不等于“没有坑”。事实是:初学者在这一步最容易犯三类错误——依赖坐标写错(org.locationtech.jts.io 与 org.locationtech.jts 混淆)、新旧包名混用(com.vividsolutions 与 org.locationtech 并存)、以及跑通了程序却不知道如何验证结果对不对。本章把这三件事彻底讲清楚,并介绍 JTS 自带的三个工具:TestBuilder、JtsOp、TestRunner——它们能在没有 IDE、没有测试框架的情况下,快速验证一个几何运算的结果。

读完后你应该能够:在 Maven/Gradle/纯 jar 三种方式下正确接入 JTS;写出第一个可运行、可调试的程序;用 JShell 做交互式验证;用 TestBuilder 可视化几何;用 JtsOp 做命令行批量运算;并知道遇到依赖冲突、类找不到、结果异常时从哪里排查。


2.1 环境要求

2.1.1 版本矩阵

JTS 本身对运行环境的要求很低,但“能跑”和“跑得顺”是两回事。下面这张表是实践中的稳妥组合:

组件 最低要求 推荐 说明
JDK / JRE Java 8 Java 11 / 17 jts-core 的字节码目标为 1.8,Java 8 即可运行
构建工具 Maven 3.5+ Maven 3.8/3.9 仅编译 JTS 源码时需要
Gradle 6.x 8.x 使用 Gradle 的项目
内存 256 MB 1 GB+ 大几何与索引会吃掉更多内存
磁盘 5 MB 50 MB jar 本身很小,源码与依赖另算

关于 JDK 版本有一个细节:JTS 的 pom.xml 中 maven.compiler.source 与 target 都是 1.8,意味着产出的 class 文件是 Java 8 字节码。你完全可以用 JDK 17 来编译和运行调用 JTS 的项目,但如果要编译 JTS 源码本身,注意 JDK 9+ 之后 -source 1.8 已被标记为过时(会有警告),JDK 21 起部分版本甚至不再支持 1.8 目标。稳妥做法是用 JDK 8 或 11 编译 JTS 源码。

2.1.2 检查本机环境

java -version
# openjdk version "17.0.9" 2023-10-17
# OpenJDK Runtime Environment (build 17.0.9+11)
# OpenJDK 64-Bit Server VM (build 17.0.9+11, mixed mode)

mvn -version
# Apache Maven 3.8.8
# Maven home: ...
# Java version: 17.0.9, vendor: Eclipse Adoptium

如果 java -version 显示的是 1.8,也完全没问题。真正需要确认的是编译目标与运行 JDK 一致:用高版本 JDK 编译、低版本 JDK 运行会报 UnsupportedClassVersionError。

2.1.3 依赖大小与内存

jts-core 的 jar 只有 1 MB 出头,这是它在嵌入式、Android、Serverless 场景受欢迎的原因之一。但要注意:几何对象本身的内存占用与顶点数量成正比。百万顶点的线串在默认堆内存下很容易 OOM,做大数据处理时要提前规划(第 20 章有专门的内存与性能讨论)。


2.2 三种接入方式

2.2.1 Maven(推荐)

在 pom.xml 中加入依赖:

<properties>
    <jts.version>1.20.0</jts.version>
</properties>

<dependencies>
    <!-- 核心:几何模型、算法、索引、WKT/WKB -->
    <dependency>
        <groupId>org.locationtech.jts</groupId>
        <artifactId>jts-core</artifactId>
        <version>${jts.version}</version>
    </dependency>
</dependencies>

需要 GeoJSON / TWKB 时,追加:

<dependency>
    <groupId>org.locationtech.jts.io</groupId>
    <artifactId>jts-io-common</artifactId>
    <version>${jts.version}</version>
</dependency>

注意 groupId 的差异:jts-core 属于 org.locationtech.jts,而 jts-io-common 属于 org.locationtech.jts.io(它的 parent 才是 org.locationtech.jts:jts-io)。这是 JTS 多模块设计留下的一个“历史包袱”,写错 groupId 会得到 “Could not find artifact” 错误。本章 2.9 节有一份完整的坐标对照表。

2.2.2 使用 SNAPSHOT 版本

如果你想试用 1.21 的新特性(CoverageCleaner、MinimumBoundingTriangle 等),需要从 Eclipse 的 LocationTech 仓库拉取快照。官方 USING.md 给出了配置:

<repositories>
    <repository>
        <id>locationtech-releases</id>
        <url>https://repo.eclipse.org/content/repositories/jts</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>jts-snapshots</id>
        <url>https://repo.eclipse.org/content/repositories/jts-snapshots</url>
        <releases>
            <enabled>false</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

然后把版本改成 1.21.0-SNAPSHOT。生产项目不要依赖 SNAPSHOT:快照每天可能变化,构建不可复现,而且没有版本保证。正确姿势是等正式版发布,或把需要的类暂时复制到自己的 lab 包中(注意遵守许可证)。

2.2.3 Gradle

dependencies {
    implementation 'org.locationtech.jts:jts-core:1.20.0'
    // 需要 GeoJSON/TWKB 时
    implementation 'org.locationtech.jts.io:jts-io-common:1.20.0'
}

Kotlin DSL 写法:

dependencies {
    implementation("org.locationtech.jts:jts-core:1.20.0")
    implementation("org.locationtech.jts.io:jts-io-common:1.20.0")
}

Gradle 的依赖仲裁比 Maven 更“聪明”一些:默认选最高版本。如果多个依赖传递引用了不同版本的 jts-core,Gradle 会选最高版本,这通常是安全的;但仍建议用 dependencyManagement(Maven)或 constraints(Gradle)显式锁定,避免“今天能跑、明天换个依赖就变版本”的问题。

2.2.4 纯 jar 与手工 classpath

不借助构建工具时,从 Maven Central 下载 jar:

# 目录结构
lib/
└── jts-core-1.20.0.jar

# 编译
javac -cp lib/jts-core-1.20.0.jar -d out src/HelloJts.java

# 运行
java -cp "lib/jts-core-1.20.0.jar;out" HelloJts     # Windows
java -cp "lib/jts-core-1.20.0.jar:out" HelloJts     # Linux/macOS

注意 Windows 用分号 ;、Linux/macOS 用冒号 : 分隔 classpath。这是跨平台脚本里最常见的错误之一。

2.2.5 在 IDE 中配置

IntelliJ IDEA:新建 Maven 项目后,在 pom.xml 里加依赖,IDEA 会自动下载并索引。若想调试 JTS 源码,可以把源码 jar 或仓库目录附加到依赖上:File → Project Structure → Libraries → jts-core → 添加 Sources,指向 D:/self/code/jts/modules/core/src/main/java。

Eclipse:File → New → Maven Project,勾选 “Create a simple project”,填入 groupId/artifactId,然后在 pom 里加依赖。

VS Code:安装 “Extension Pack for Java” 与 “Maven for Java”,打开含 pom.xml 的目录即可。

NetBeans:File → New Project → Java with Maven,后续同理。

2.2.6 Android 与 Kotlin

jts-core 是纯 Java 字节码,Android 可以直接使用:

android {
    // ...
}
dependencies {
    implementation 'org.locationtech.jts:jts-core:1.20.0'
}

注意 Android 的 minSdkVersion 与 Java 8 语言特性支持(desugar_jdk_libs)。JTS 本身不依赖 Java 8 的 lambda 之外的特性,兼容性良好。Kotlin 项目直接使用即可,Kotlin 的扩展函数可以给 Geometry 加上更符合习惯的包装。


2.3 第一个程序:完整可运行版本

2.3.1 工程结构

从零开始建一个最小工程:

jts-demo/
├── pom.xml
└── src/
    └── main/
        └── java/
            └── demo/
                └── QuickStart.java

2.3.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>demo</groupId>
    <artifactId>jts-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jts.version>1.20.0</jts.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>${jts.version}</version>
        </dependency>
    </dependencies>
</project>

2.3.3 QuickStart.java

package demo;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;

public class QuickStart {

    private static final GeometryFactory GF = new GeometryFactory();
    private static final WKTReader READER = new WKTReader(GF);
    private static final WKTWriter WRITER = new WKTWriter();

    public static void main(String[] args) throws Exception {
        demo1ParseAndPrint();
        demo2CreateFromCoordinates();
        demo3Relations();
        demo4Buffer();
    }

    /** 演示 1:WKT 解析与输出 */
    private static void demo1ParseAndPrint() throws Exception {
        Geometry g = READER.read("LINESTRING (0 0, 10 10, 20 0)");
        System.out.println("[1] 类型 = " + g.getGeometryType());
        System.out.println("[1] 长度 = " + g.getLength());          // 28.284...
        System.out.println("[1] 包络 = " + g.getEnvelopeInternal()); // Envelope[0 : 20, 0 : 10]
        System.out.println("[1] WKT = " + WRITER.write(g));
    }

    /** 演示 2:用坐标数组创建几何 */
    private static void demo2CreateFromCoordinates() {
        Coordinate[] coords = new Coordinate[] {
            new Coordinate(0, 0),
            new Coordinate(10, 0),
            new Coordinate(10, 10),
            new Coordinate(0, 10),
            new Coordinate(0, 0)
        };
        Polygon polygon = GF.createPolygon(coords);
        System.out.println("[2] 面积 = " + polygon.getArea());   // 100.0
        System.out.println("[2] 周长 = " + polygon.getLength()); // 40.0
        Point centroid = polygon.getCentroid();
        System.out.println("[2] 质心 = " + centroid);            // POINT (5 5)
    }

    /** 演示 3:空间关系判断 */
    private static void demo3Relations() throws Exception {
        Geometry a = READER.read("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))");
        Geometry b = READER.read("POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))");
        System.out.println("[3] a 与 b 相交 = " + a.intersects(b));   // true
        System.out.println("[3] a 包含 (6,6) = "
            + a.contains(GF.createPoint(new Coordinate(6, 6))));      // true
        System.out.println("[3] 交集面积 = " + a.intersection(b).getArea()); // 25.0
    }

    /** 演示 4:缓冲区 */
    private static void demo4Buffer() throws Exception {
        LineString line = (LineString) READER.read("LINESTRING (0 0, 10 0)");
        Geometry buffered = line.buffer(1.0);
        System.out.println("[4] 缓冲后类型 = " + buffered.getGeometryType()); // Polygon
        System.out.println("[4] 缓冲后面积 ≈ " + buffered.getArea());        // ≈ 20 + π
    }
}

2.3.4 运行与输出

mvn -q compile exec:java -Dexec.mainClass=demo.QuickStart

或直接在 IDE 里运行 main。输出:

[1] 类型 = LineString
[1] 长度 = 28.284271247461902
[1] 包络 = Envelope[0.0 : 20.0, 0.0 : 10.0]
[1] WKT = LINESTRING (0 0, 10 10, 20 0)
[2] 面积 = 100.0
[2] 周长 = 40.0
[2] 质心 = POINT (5 5)
[3] a 与 b 相交 = true
[3] 交集面积 = 25.0
[4] 缓冲后类型 = Polygon
[4] 缓冲后面积 ≈ 22.283185307179585

如果输出与此一致,说明环境完全没问题。注意 [2] 质心 = POINT (5 5) 的打印格式来自 Point.toString(),它等价于 WKT 输出。

2.3.5 这段代码示范了什么

演示 覆盖的知识点 对应章节
1 WKT 解析、长度、包络、序列化 第 6 章
2 GeometryFactory 创建、面积/长度/质心 第 4、5、11 章
3 空间谓词、叠加 第 8、9 章
4 缓冲区 第 10 章

一个值得注意的细节:GeometryFactory、WKTReader、WKTWriter 都是线程安全的(无状态或内部状态只读),可以定义为 static final 复用。但 Geometry 对象本身不是严格线程安全的——不是说不能多线程读,而是不要在多线程间“一边算一边改”。这个区别在第 20 章会展开。


2.4 JShell 交互式体验

2.4.1 启动 JShell

JDK 9+ 自带 JShell,非常适合快速验证 API:

# 把 jts-core 加入 classpath 后启动
jshell --class-path lib/jts-core-1.20.0.jar

如果使用 Maven 项目,可以用:

mvn dependency:build-classpath -Dmdep.outputFile=cp.txt
jshell --class-path "lib/jts-core-1.20.0.jar:$(cat cp.txt)"

Windows 下路径分隔符换成 ;。

2.4.2 常用片段

import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.*;

WKTReader r = new WKTReader();
Geometry a = r.read("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))");
Geometry b = r.read("POLYGON ((5 0, 15 0, 15 10, 5 10, 5 0))");

a.getArea();          // 100.0
a.intersection(b);    // POLYGON ((5 0, 10 0, 10 10, 5 10, 5 0))
a.distance(b);        // 0.0
a.relate(b);          // 212101212  (DE-9IM 矩阵)

JShell 会实时打印表达式结果,非常适合“这个方法到底返回什么”的探索。比写一个 main 再运行快得多。

2.4.3 JShell 的局限

  • 不能方便地调试(没有断点);
  • 类路径需要手动维护,依赖多了很麻烦;
  • 长代码不好管理。

所以 JShell 适合“验证单个 API”,不适合做完整实验。要做完整实验,还是建一个 Maven 工程,或者用 TestBuilder。


2.5 JTS TestBuilder:可视化试验场

2.5.1 它是什么

TestBuilder 是 JTS 自带的 Swing GUI 工具(modules/app),能:

  • 手工输入/编辑 WKT 几何,或从文件加载;
  • 执行 JTS TestBuilder 中注册的所有函数(几乎覆盖 JTS 全部能力);
  • 对比运算前后的几何,查看结果 WKT;
  • 运行 XML 测试用例,查看通过/失败;
  • 可视化显示几何、顶点、标签。

对学习 JTS 来说,这是最高效的“所见即所得”工具。官方也用它做算法演示与 bug 复现。

2.5.2 获取与启动

方式一:从源码构建(需要 Maven):

cd D:/self/code/jts
mvn clean install -DskipTests
# 生成 modules/app/target/JTSTestBuilder.jar

启动(官方推荐带上内存参数):

java -jar modules/app/target/JTSTestBuilder.jar -Xmx2000M

注意 -Xmx2000M 要放在 -jar 之后,这是 TestBuilder 自己解析的参数。

macOS 上如果界面怪异,可以指定 Metal 外观:

java -Dswing.defaultlaf=javax.swing.plaf.metal.MetalLookAndFeel \
     -jar modules/app/target/JTSTestBuilder.jar -Xmx2000M

方式二:用 bin 脚本:仓库 bin/ 目录下有 testbuilder.sh/testbuilder.bat```,内部已封装启动命令。

方式三:直接下载 Release:从 GitHub Releases 页面下载带 TestBuilder 的发行包。

2.5.3 界面速览

TestBuilder 的界面分为几个区域(不同版本略有差异):

┌────────────────────────────────────────────────────────────┐
│ 菜单栏:File / Edit / View / Tools ...                      │
├──────────────────────────────┬─────────────────────────────┤
│                              │  函数面板(Function Tree)   │
│        几何显示区             │  ├ Geometry                 │
│   (A、B、结果三种颜色)       │  ├ Overlay                  │
│                              │  ├ Buffer                   │
│                              │  └ ...                      │
├──────────────────────────────┴─────────────────────────────┤
│  几何输入区:A 的 WKT / B 的 WKT / 结果 WKT                  │
├────────────────────────────────────────────────────────────┤
│  状态栏:坐标、提示信息                                       │
└────────────────────────────────────────────────────────────┘

2.5.4 一次典型操作

  1. 在 A 输入框粘贴:POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))
  2. 在 B 输入框粘贴:POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))
  3. 在函数树里找到 Overlay → Intersection,点击执行;
  4. 结果框出现交集的 WKT,显示区高亮画出交集区域;
  5. 想调参数?Buffer → Buffer 的函数的参数框会显示 distance 等参数,改完再执行。

这个“改参数看结果”的循环,比在 IDE 里改代码快一个数量级,特别适合回答“这个参数到底怎么影响结果”。

2.5.5 XML 测试与 TestRunner

TestBuilder 还能加载 XML 测试文件(.xml),逐个用例执行并与期望结果比对。XML 测试是 JTS 官方的主要回归测试形式之一(另一种是 JUnit),格式大致如下:

<run>
  <case>
    <desc>buffer of a point</desc>
    <a>POINT (0 0)</a>
    <test><op name="buffer" arg1="A" arg2="1">POLYGON ((...))</op></test>
  </case>
</run>

命令行版本是 TestRunner:

cd D:/self/code/jts
bin/testrunner.sh -files modules/tests/src/test/resources/testxml/general/TestBuffer.xml

Windows 用 bin\testrunner.bat。TestRunner 的输出会列出每个用例的期望值、实际值、通过状态。第 21 章讲测试体系时会详细展开。

2.5.6 TestBuilder 的价值与局限

适合 不适合
快速验证一个函数的语义 批量数据处理
复现 bug(保存成 XML 用例) 集成进 CI
观察几何形状与顶点 性能测试(GUI 开销大)
学习算法参数的影响 生产环境部署(Swing GUI)

2.6 JtsOp:命令行几何计算器

2.6.1 它是什么

JtsOp 是一个 CLI 工具,把 TestBuilder 里注册的函数搬到了命令行。它能读 WKT/WKB/GeoJSON/GML/SHP,执行任意一个函数,输出 WKT/WKB/GeoJSON/GML/SVG,还能用管道串联多个操作。

对运维、批处理、快速验证来说,这比写 Java 代码快得多。

2.6.2 启动

cd D:/self/code/jts
bin/jtsop.sh -help

Windows 用 bin\jtsop.bat -help。它会打印用法和所有可用函数列表(这个列表很长,就是 TestBuilder 的函数树)。

2.6.3 常用示例

# 计算 WKT 的面积
jtsop -a "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))" -f txt area

# 两个几何求交,输出 WKT
jtsop -a "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))" \
      -b "POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))" \
      -f wkt Overlay.intersection

# 缓冲区,输出 GeoJSON
jtsop -a "POINT (10 10)" -f geojson Buffer.buffer 10

# 批量对文件里的几何做 unary union
jtsop -a geoms.wkt -collect -f wkb Overlay.unaryUnion

# 校验一批几何的有效性
jtsop -a geoms.wkt -limit 100 -offset 40 -f txt Geometry.isValid

# 用管道串联:生成随机点 → 缓冲区
jtsop -f wkb CreateRandomShape.randomPoints 10 | jtsop -a stdin -f wkt Buffer.buffer 1

2.6.4 输出格式与选项

选项 作用
-a / -b 指定 A/B 输入(字面量、文件名或 stdin)
-f 输出格式:wkt、wkb、geojson、gml、svg、txt
-v 输出输入信息与耗时
-time 只输出耗时
-index 二元运算使用空间索引
-collect 把 A 收集成一个 GeometryCollection
-eacha / -eachb 把输入拆成单个几何逐个处理
-explode 结果拆成多个几何输出
-srid 给输出设置 SRID
-args 一组参数多值执行(用于调参)
-repeat 重复执行 n 次(用于测性能)
-geomfunc 动态加载自定义函数类

2.6.5 一个真实用途:结果比对

假设你把 JTS 从 1.19 升级到 1.20,担心某个 buffer 结果变了。可以用 JtsOp 对同一批数据跑两遍(两个版本的 jar),再 diff:

# 用 1.19 跑
java -cp jts-core-1.19.0.jar:jts-app.jar ... jtsop -a data.wkt -f wkt Buffer.buffer 5 > out-119.txt

# 用 1.20 跑
java -cp jts-core-1.20.0.jar:jts-app.jar ... jtsop -a data.wkt -f wkt Buffer.buffer 5 > out-120.txt

diff out-119.txt out-120.txt

这比写测试框架轻,比肉眼看几何靠谱。第 21 章会把这个思路扩展成正式的升级验证流程。


2.7 从源码构建 JTS

2.7.1 为什么需要自己构建

三种情况需要:

  1. 想读/改 JTS 源码(第 21、22 章);
  2. 想要 TestBuilder / JtsOp 工具(发行包里不一定带);
  3. 需要未发布的 SNAPSHOT 特性。

2.7.2 构建命令

官方 DEVELOPING.md 给了标准命令(在仓库根目录执行):

# 完整构建(含测试)
mvn clean install

# 跳过测试(快)
mvn clean install -DskipTests

# 只编译并安装 core 模块
mvn install -pl modules/core -am

# 构建带 Oracle/SDE 支持的 io 模块(需要专有驱动)
mvn install -Poracle
mvn install -Parcsde

# 构建所有 profile
mvn install -Dall=true

# 只构建发布产物
mvn install -Drelease=true

2.7.3 质量检查

JTS 的 mvn verify 会跑 PMD、Checkstyle 等静态检查:

mvn verify

# 跳过静态检查(只跑测试)
mvn verify -Dpmd.skip=true -Dcheckstyle.skip=true

如果只是想看某个模块的报告:

mvn site:site
# 然后打开 modules/core/target/site/index.html

2.7.4 构建产物

modules/core/target/jts-core-1.21.0-SNAPSHOT.jar
modules/io/common/target/jts-io-common-1.21.0-SNAPSHOT.jar
modules/app/target/JTSTestBuilder.jar
modules/tests/target/jts-tests-1.21.0-SNAPSHOT.jar

构建完成后,本地 Maven 仓库(~/.m2/repository)里会有 SNAPSHOT 版本,其他项目可以直接引用。

2.7.5 常见构建问题

现象 原因 解决
Fatal error compiling: invalid target release: 1.8 JDK 版本过高 换 JDK 8/11,或改用 --release 8
下载依赖卡住 网络/代理 配置 Maven settings.xml 镜像
invalid LOC header jar 下载损坏 删除 ~/.m2 中对应目录重下
TestBuilder 启动黑屏 缺少图形环境 / 字体 用 -Dswing.defaultlaf 换外观;无头服务器无法运行 GUI
构建成功但找不到类 只 mvn compile 未 install 用 mvn install 或 -am

2.8 调试与验证技巧

2.8.1 打印几何的三种方式

Geometry g = ...;

// 1. 默认 toString():标准 WKT 风格
System.out.println(g);

// 2. WKTWriter:可控格式
System.out.println(new WKTWriter().write(g));

// 3. 调试用详细信息
System.out.println("type=" + g.getGeometryType()
    + ", srid=" + g.getSRID()
    + ", numPoints=" + g.getNumPoints()
    + ", area=" + g.getArea()
    + ", length=" + g.getLength()
    + ", envelope=" + g.getEnvelopeInternal());

2.8.2 断点调试点

JTS 的类层次不深,调试时建议在以下位置下断点:

  • Geometry.union/intersection 的入口(看是否走了 overlayng);
  • OverlayOp.overlay / OverlayNGRobust.overlay(看引擎);
  • RobustLineIntersector(看精度问题);
  • IsValidOp.checkValid(看无效原因)。

2.8.3 结果“看起来不对”时

按这个顺序排查(与第 18 章呼应):

  1. 坐标系:是不是经纬度直接算面积/距离(第 19 章);
  2. 几何有效性:用 IsValidOp 检查(第 17 章);
  3. 引擎:是否期望新引擎但实际走了旧的(本章 1.6 节、第 9 章);
  4. 精度:是否需要 PrecisionModel 或 snap-rounding(第 18 章);
  5. 版本:是不是 SNAPSHOT 行为变化(本章 2.2.2)。

这五步能排掉 90% 的“玄学问题”。


2.9 Maven 坐标对照表

JTS 的模块坐标容易记混,这里给一张完整表(版本统一用同一个属性):

模块 groupId artifactId 说明
父 POM org.locationtech.jts jts 版本管理
核心 org.locationtech.jts jts-core 必引
IO 父模块 org.locationtech.jts jts-io 打包组织
IO 通用 org.locationtech.jts.io jts-io-common GeoJSON/TWKB
Oracle IO org.locationtech.jts.io jts-io-ora 需专有驱动
SDE IO org.locationtech.jts.io jts-io-sde 需专有驱动
TestBuilder org.locationtech.jts jts-app GUI 工具
测试套件 org.locationtech.jts jts-tests XML 测试
实验功能 org.locationtech.jts jts-lab 不稳定 API

小抄:只有 core/app/tests/lab 在 org.locationtech.jts 下,所有 io 模块在 org.locationtech.jts.io 下。


2.10 内网与离线环境接入

2.10.1 为什么需要离线方案

JTS 本身很小,但在一些场景下无法访问 Maven Central:内网构建机、涉密环境、CI 缓存受限、或者只是想让构建可复现。JTS 的优势在于依赖极少,离线方案因此特别简单。

2.10.2 方案一:本地仓库预置

在有网机器上下载 jar 与 pom,拷进内网机器的本地仓库目录:

# 有网机器:下载依赖到指定目录
mvn dependency:copy -Dartifact=org.locationtech.jts:jts-core:1.20.0 \
    -DoutputDirectory=./offline-libs

# 内网机器:手工安装进本地仓库
mvn install:install-file \
    -Dfile=./offline-libs/jts-core-1.20.0.jar \
    -DgroupId=org.locationtech.jts \
    -DartifactId=jts-core \
    -Dversion=1.20.0 \
    -Dpackaging=jar

之后内网项目就能像正常一样引用依赖。

2.10.3 方案二:system scope(不推荐)

<dependency>
    <groupId>org.locationtech.jts</groupId>
    <artifactId>jts-core</artifactId>
    <version>1.20.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/jts-core-1.20.0.jar</systemPath>
</dependency>

system scope 会在打包时被排除(除非额外配置),且 Maven 官方已不推荐,仅作为临时手段。

2.10.4 方案三:私有仓库代理

企业内网更规范的做法是搭 Nexus/Artifactory 代理 Maven Central,或使用阿里云/华为云的镜像:

<mirrors>
    <mirror>
        <id>aliyun</id>
        <mirrorOf>central</mirrorOf>
        <url>https://maven.aliyun.com/repository/central</url>
    </mirror>
</mirrors>

2.10.5 方案四:直接把源码编译进项目

如果只需要少量类(例如只想要 WKT 读写),可以把对应的几个源文件复制到自己的包下(注意遵守 EPL/EDL 的版权声明要求)。这种做法在 Android、小程序后端等对包体积敏感的场景偶尔可见,但要承担后续升级维护成本。

2.10.6 校验 jar 完整性

# 查看 jar 内容
jar tf jts-core-1.20.0.jar | head -20

# 校验 sha1(Maven Central 提供 .sha1 文件)
sha1sum jts-core-1.20.0.jar

2.11 依赖冲突排查实战

2.11.1 典型症状

JTS 的依赖冲突通常不报“找不到类”,而是报方法找不到或行为异常:

症状 可能原因
NoSuchMethodError: Geometry.getZ() 运行期加载了 1.15 之前的旧 jar
NoClassDefFoundError: org/locationtech/jts/.. 依赖被其他库的 exclude 掉了
ClassCastException 涉及 com.vividsolutions 新旧包名混用
同一方法在两个版本行为不同 传递依赖把 jts-core 降级了

2.11.2 第一步:看依赖树

# Maven:只显示 jts 相关
mvn dependency:tree -Dincludes=org.locationtech.jts

# 输出示例:
# [INFO] +- org.locationtech.jts:jts-core:jar:1.20.0:compile
# [INFO] |  \- (无传递依赖)
# [INFO] +- com.example:geo-utils:jar:1.0:compile
# [INFO] |  \- org.locationtech.jts:jts-core:jar:1.16.1:compile   ← 冲突来源

Gradle 用 gradle dependencies --configuration runtimeClasspath | grep jts。

2.11.3 第二步:锁定版本

Maven 在父 pom 的 dependencyManagement 里锁定:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>1.20.0</version>
        </dependency>
        <dependency>
            <groupId>org.locationtech.jts.io</groupId>
            <artifactId>jts-io-common</artifactId>
            <version>1.20.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

Gradle 用 constraints 或 resolutionStrategy.force。

2.11.4 第三步:排除旧依赖

<dependency>
    <groupId>com.example</groupId>
    <artifactId>geo-utils</artifactId>
    <version>1.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2.11.5 第四步:运行时确认

// 确认运行期实际加载的 JTS 版本
Package pkg = org.locationtech.jts.JTSVersion.class.getPackage();
System.out.println(pkg.getImplementationVersion());

// 或直接打印版本常量
System.out.println(org.locationtech.jts.JTSVersion.CURRENT_VERSION);

如果打印出来的版本与 pom 里写的不一致,就是依赖仲裁出了问题。

2.11.6 一个真实案例

某项目同时引入了 GeoTools 与一个自研工具库,后者依赖 JTS 1.16。Maven 的“最近优先”策略让 JTS 1.16 胜出,结果 GeoTools 里调用的 RelateNG 直接 NoSuchMethodError。修复方式是在父 pom 锁定 1.20.0 并排除自研库里的旧依赖——升级 JTS 版本时,务必确认所有下游库都兼容。


2.12 常见问题与避坑

2.10.1 依赖与类加载

Q1:Could not find artifact org.locationtech.jts:jts-io-common? groupId 写错了,应该是 org.locationtech.jts.io:jts-io-common。

Q2:NoClassDefFoundError: org/locationtech/jts/geom/Geometry? 依赖没进 classpath。Maven 项目检查 mvn dependency:tree;纯 jar 检查 -cp。

Q3:项目里同时存在新旧包名? 检查依赖树里有没有还停留在 1.14 及更早的库(com.vividsolutions:jts)。用 mvn dependency:tree | grep -i jts 排查,统一升级。

Q4:多个库依赖不同版本的 jts-core 怎么办? 在父 pom 的 dependencyManagement 中锁定版本,这是最可靠的做法。

2.10.2 运行与输出

Q5:UnsupportedClassVersionError? 编译 JDK 版本高于运行 JDK 版本。统一版本,或降低 target。

Q6:中文乱码? Windows 终端默认编码可能是 GBK。加 -Dfile.encoding=UTF-8@,或在 IDE 中设置控制台编码。

Q7:Geometry.toString() 输出的坐标精度很高? 默认保留坐标原始值。需要控制精度用 WKTWriter 的 setPrecisionModel / setFormatted(第 6 章)。

2.10.3 工具使用

Q8:TestBuilder 启动后窗口空白? 可能是无头环境(服务器)或缺少图形库。TestBuilder 是 Swing 程序,必须在有桌面环境的机器上运行。

Q9:JtsOp 找不到函数名? 用 jtsop -help 查看完整函数列表;大小写敏感,函数名通常形如 Overlay.intersection、Buffer.buffer。

Q10:TestRunner 与 TestBuilder 的关系? TestRunner 是命令行版,专跑 XML 测试;TestBuilder 是 GUI 版,可以手工调试同一个 XML。两者共享同一套测试引擎。


2.13 本章小结

  • JTS 接入极简:一个 jts-core jar 即可,JDK 8+ 可运行;
  • Maven 坐标要记牢:core 在 org.locationtech.jts,io 在 org.locationtech.jts.io;
  • 老项目迁移注意包名(com.vividsolutions → org.locationtech)与坐标变化;
  • JShell 适合验证单个 API,TestBuilder 适合可视化调试,JtsOp 适合命令行/批处理,TestRunner 适合 XML 回归测试;
  • 三个工具都来自 JTS 自身的 modules/app 与 modules/tests,构建 mvn clean install 后可用;
  • 从源码构建耗时但值得,尤其当你需要读源码或改源码时;
  • 结果异常先查五件事:坐标系、有效性、引擎、精度、版本。

相关资源

  • JTS 用户指南:https://github.com/locationtech/jts/blob/master/USING.md
  • JTS 开发指南:https://github.com/locationtech/jts/blob/master/DEVELOPING.md
  • JTS 工具说明:https://github.com/locationtech/jts/blob/master/doc/TOOLS.md
  • JtsOp 用户指南:https://github.com/locationtech/jts/blob/master/doc/JTSOp.md
  • TestBuilder 说明:https://github.com/locationtech/jts/blob/master/doc/JTSTestBuilder.md
  • Maven Central:https://mvnrepository.com/artifact/org.locationtech.jts
  • 上一章:第01章 - JTS 概述与生态定位
  • 下一章:第03章 - 核心架构与源码结构