第03章 - 核心架构与源码结构

前两章解决了“是什么”和“怎么跑起来”。从这一章开始进入源码视角:JTS 这个 62 万行(含测试)的工程,到底是怎么组织的?为什么同样的功能有时有两条实现路径?一个 Geometry.union() 调用,内部要经过多少层?

很多开发者用了几年 JTS,对它的印象仍是“一堆 Geometry 类 + 一堆静态方法”。这种印象会导致两个后果:遇到问题时不知道去哪个包找答案;想扩展时不知道从哪里插入。真正的“会用”分三层——会调 API、知道实现在哪、理解为什么这样设计。本章负责后两层。

本章不追求把每个包都讲透(后续章节会分头深入),而是给你一张可用的“源码地图”和一套阅读方法。读完后你应该能够:拿到一个需求能立刻定位到 org.locationtech.jts 下的目标包;判断一次几何运算是走哪条引擎路径;看懂模块依赖并知道为什么 lab 模块不稳定;独立把 JTS 源码跑起来并开始按图索骥。


3.1 仓库全景

3.1.1 顶层目录

打开 JTS 仓库根目录(本教程以 D:/self/code/jts 这份 1.21.0-SNAPSHOT 检出为准),会看到:

jts/
├── pom.xml                父 POM,聚合 build-tools 与 modules
├── build-tools/           构建期组件:PMD/Checkstyle 规则、license header
├── modules/               所有功能模块
│   ├── core/              jts-core(几何内核)
│   ├── io/                jts-io-common / jts-io-ora / jts-io-sde
│   ├── app/               jts-app(TestBuilder GUI)
│   ├── tests/             jts-tests(XML 测试套件 + TestRunner)
│   ├── example/           jts-example(示例代码)
│   └── lab/               jts-lab(实验性算法)
├── doc/                   文档:开发者指南、技术规范、版本历史、工具说明
├── bin/                   启动脚本:jtsop、testbuilder、testrunner
├── README.md              项目总览
├── USING.md               用户指南(模块说明、Maven 用法)
├── DEVELOPING.md          开发者指南(构建、测试)
├── MIGRATION.md           升级指南(包名/坐标变更)
├── CONTRIBUTING.md        贡献指南
├── RELEASING.md           发布流程
├── LICENSES.md            许可证说明
└── FAQ-LICENSING.md       许可证 FAQ

3.1.2 规模统计

以 1.21.0-SNAPSHOT 检出的实测数据:

统计项 数量 备注
core 主源码 625 个 .java modules/core/src/main/java
core 单元测试 388 个 .java modules/core/src/test/java
XML 测试用例 129 个 .xml modules/tests/src/test/resources/testxml
app(TestBuilder) 275 个 .java modules/app/src/main/java
tests 49 个 .java 测试引擎与 TestRunner
lab 23 个 .java 实验算法
io/common 9 个 .java GeoJSON、TWKB、WKB 辅助

这些数字本身不重要,重要的是比例:测试代码量接近主代码量,这是 JTS 能保持高可靠性的原因。读源码时如果看不懂某个算法的意图,去看对应的测试,往往比看 Javadoc 有效。

3.1.3 文档目录

doc/ 目录里有几份关键文档,值得先泛读:

文件 内容 什么时候看
JTS_Version_History.md 每个版本的变更清单 判断某特性从哪个版本开始有
JTSOp.md 命令行工具用法 做批量验证/对比
JTSTestBuilder.md 可视化工具用法 调试几何
TOOLS.md 工具总览 找工具入口
JTS Developer Guide.pdf 开发者指南 想理解算法设计
JTS Technical Specs.pdf 技术规范 研究内部数据结构

PDF 是历史文档,部分内容已过时(比如还在讲旧的 overlay 实现),阅读时注意与当前源码核对。


3.2 模块划分与依赖

3.2.1 依赖关系图

                    ┌──────────────┐
                    │  jts-tests   │  XML 测试套件 + TestRunner
                    └──────┬───────┘
                           │ depends on
                    ┌──────▼───────┐        ┌──────────────┐
                    │   jts-core   │◄───────┤  jts-example │
                    └──────┬───────┘        └──────────────┘
         ┌─────────────────┼──────────────────┐
         │                 │                  │
┌────────▼───────┐ ┌───────▼────────┐ ┌───────▼───────┐
│ jts-io-common  │ │    jts-app     │ │    jts-lab    │
│ GeoJSON/TWKB   │ │  TestBuilder   │ │  实验算法      │
└────────────────┘ └───────┬────────┘ └───────────────┘
                           │
                    ┌──────▼───────┐
                    │  jts-tests   │
                    └──────────────┘

3.2.2 依赖矩阵

实测各模块 pom 中的直接依赖:

模块 依赖 说明
core 无(仅测试期 junit) 零运行时依赖,可嵌入任何环境
io/common jts-core、json-simple、commons-csv GeoJSON 解析用 json-simple
io/ora jts-core、ojdbc8 需 -Poracle profile
io/sde jts-core、ArcSDE API 需 -Parcsde profile,闭源依赖
app jts-core、jts-tests、jts-io-common、jdom2 TestBuilder 需要 XML 解析
tests jts-core、jdom2、commons-lang3 XML 测试引擎
lab jts-core、jdom2 实验算法
example jts-core 示例

core 零运行时依赖是 JTS 最有价值的工程特性之一:它意味着你可以在 Android、OSGi、Java Module System、GraalVM、甚至 Applet(历史场景)里使用它,不受传递依赖冲突的影响。

3.2.3 为什么 lab 不稳定

modules/lab 收录的是尚未经过充分验证、或 API 可能变化的算法,典型如:

  • org.locationtech.jtslab.clip:矩形裁剪多边形;
  • org.locationtech.jtslab.edgeray:边射线求交面积;
  • org.locationtech.jtslab.clean:小洞移除、无效洞移除;
  • org.locationtech.jtslab.OverlayAreaFunctions:叠加面积计算实验。

这些类的问题是:它们没有进入 core 的 API 稳定性承诺。一旦算法成熟,它们会被“毕业”移入 core(例如 1.20 的 Coverage 系列就是从 lab 毕业的);如果被证明有缺陷,可能直接删除。因此生产代码不要依赖 jts-lab,除非你愿意承担 API 变动风险。


3.3 core 包结构详解

3.3.1 顶层包地图

modules/core/src/main/java/org/locationtech/jts 下共有 20 个直接子包(截至 1.21.0-SNAPSHOT):

包 中文职责 关键类 后续章节
geom 几何模型 Geometry、Point、Polygon、Coordinate、Envelope、GeometryFactory 第 4、5 章
geom/impl 坐标序列实现 CoordinateArraySequence、PackedCoordinateSequence 第 4 章
geom/prep 预处理几何 PreparedGeometry、PreparedGeometryFactory 第 8、20 章
geom/util 几何工具 GeometryEditor、GeometryFixer、AffineTransformation 第 5、17 章
algorithm 基础算法 Distance、Centroid、ConvexHull、RobustLineIntersector 第 11、12 章
algorithm/hull 凹包 ConcaveHull、ConcaveHullOfPolygons 第 12 章
algorithm/distance 距离度量 DiscreteHausdorffDistance、DirectedHausdorffDistance 第 11 章
algorithm/locate 点定位 IndexedPointInAreaLocator 第 8 章
algorithm/construct 构造算法 MaximumInscribedCircle、LargestEmptyCircle 第 11 章
algorithm/match 匹配 几何匹配相关 第 23 章
index 空间索引 STRtree、Quadtree、HPRtree、KdTree 第 13 章
io 序列化 WKTReader、WKBReader、WKTWriter 第 6 章
noding 节点化 MCIndexNoder、SnapRoundingNoder 第 14 章
operation 运算总目录 buffer、overlay、overlayng、relate、relateng、valid、union、distance、polygonize、linemerge 第 8–12、17 章
triangulate 三角剖分 DelaunayTriangulationBuilder、VoronoiDiagramBuilder 第 15 章
linearref 线性参考 LengthIndexedLine、LocationIndexedLine 第 16 章
simplify 化简 DouglasPeuckerSimplifier、TopologyPreservingSimplifier 第 12 章
precision 精度处理 PrecisionModel、GeometryPrecisionReducer 第 18 章
coverage 覆盖质检 CoverageValidator、CoverageUnion、CoverageSimplifier 第 17 章
math 数值工具 DD、MathUtil 第 18 章
util 杂项 Assert、GeometricShapeFactory 全书
awt AWT 桥接 ShapeReader、ShapeWriter、PointShapeFactory 第 20 章
densify 加密 Densifier 第 12 章
dissolve 线溶解 LineDissolver 第 14 章
edgegraph 半边图 HalfEdge、EdgeGraph 第 14 章
geomgraph 旧几何图 PlanarGraph、Edge、Node(旧引擎基础设施) 第 9 章
planargraph 平面图 PlanarGraph、DirectedEdge 第 14 章
shape 形状构造 GeometricShapeFactory、random、fractal 第 20 章

注:表中“后续章节”是阅读建议,不是严格依赖。

3.3.2 包名与职责的对应规律

JTS 的包名基本遵循“名词 = 数据结构,动词 = 算法”的规律:

  • geom(几何)、index(索引)、noding(节点化结构)、triangulate(三角剖分结构)——偏数据结构;
  • operation(运算)、algorithm(算法)、simplify(化简)、precision(精度)——偏算法;
  • io(输入输出)、util(工具)、math(数学)——偏支撑。

一个实践技巧:需求里的动词往往直接对应包名。“合并”“裁剪”想到 operation;“简化”想到 simplify;“查最近”想到 index;“修复无效”想到 operation/valid 或 geom/util。

3.3.3 operation 包的组织

operation 是全书出现频率最高的包,它的子包按“运算类别”组织:

operation/
├── buffer/           缓冲与偏移曲线
├── distance/         距离计算
├── distance3d/       3D 距离
├── linemerge/        线合并
├── overlay/          ★ 旧叠加引擎(geomgraph 体系)
├── overlayng/        ★ 新叠加引擎(标签边图体系)
├── polygonize/       线构面
├── predicate/        快速矩形谓词
├── relate/           ★ 旧关系判断(RelateOp)
├── relateng/         ★ 新关系判断(RelateNG,1.20+)
├── union/            并集策略(CascadedPolygonUnion 等)
└── valid/            有效性检查

带 ★ 的四个包是 JTS 最核心也最复杂的部分。它们的共同点是:都涉及“两个几何的关系/组合”,都需要处理精度与鲁棒性。

3.3.4 一份可以直接用的“需求 → 包”映射表

需求 包路径 关键类
判断相交/包含/相离 operation.relate 或 operation.relateng RelateOp、RelateNG
求交集/并集/差集 operation.overlayng(推荐)或 operation.overlay OverlayNGRobust、OverlayOp
缓冲区 operation.buffer BufferOp、BufferParameters
偏移曲线 operation.buffer OffsetCurve(1.19+)
合并线串 operation.linemerge LineMerger、LineSequencer
线构面 operation.polygonize Polygonizer
有效性检查 operation.valid IsValidOp、IsSimpleOp
修复几何 geom.util GeometryFixer
覆盖质检 coverage CoverageValidator、CoverageGapFinder
距离 operation.distance DistanceOp、IndexedFacetDistance
豪斯多夫距离 algorithm.distance DiscreteHausdorffDistance
化简 simplify DouglasPeuckerSimplifier
凸包 algorithm ConvexHull
凹包 algorithm.hull ConcaveHull
三角剖分 triangulate DelaunayTriangulationBuilder
Voronoi triangulate VoronoiDiagramBuilder
空间索引 index.strtree 等 STRtree
线性参考 linearref LengthIndexedLine
WKT/WKB io WKTReader、WKBReader
GeoJSON/TWKB io 模块 jts-io-common GeoJsonReader、TWKBReader
精度归约 precision GeometryPrecisionReducer

建议把这张表存到笔记里,比每次翻 Javadoc 快得多。


3.4 几何模型的设计

3.4.1 类层次

Geometry (abstract)
├── Point
├── LineString
│   └── LinearRing
├── Polygon
└── GeometryCollection
    ├── MultiPoint
    ├── MultiLineString
    └── MultiPolygon

关键要点:

  1. 所有几何都继承 Geometry,共享一套元数据与通用方法(SRID、工厂、包络、维度、谓词)。
  2. GeometryCollection 既是容器又是几何;Multi* 是它的特化子类。
  3. LinearRing 是 LineString 的子类,但强制闭合(首尾坐标相同)。
  4. Polygon 由 1 个 shell + n 个 hole 组成,每个环都是 LinearRing。

3.4.2 组合关系

GeometryFactory ──创建──► Geometry
                            │
        ┌───────────────────┼────────────────────┐
        ▼                   ▼                    ▼
   CoordinateSequence   PrecisionModel        SRID (int)
   (坐标存储)            (计算精度)            (元数据)

三个“外挂”决定了几何的行为:

  • CoordinateSequence:底层坐标数组的抽象,决定 X/Y/Z/M 的存储方式与访问性能;
  • PrecisionModel:决定创建时的坐标舍入规则(FLOATING 不舍入、FIXED 按 scale 舍入);
  • SRID:整数标签,不参与运算(见第 1 章 1.8 节)。

3.4.3 为什么用工厂而不是构造函数

JTS 1.15 之后,Geometry 及其子类的公开构造函数都被标记为 @Deprecated 或改为受保护,推荐统一用 GeometryFactory 创建。原因:

  1. 共享配置:所有由同一工厂创建的几何共享 SRID、PrecisionModel、CoordinateSequenceFactory,避免“每个对象各配一套”的混乱;
  2. 保证一致性:几何运算的结果会沿用输入几何的工厂,保证 SRID 不丢;
  3. 便于优化:工厂可以缓存/复用内部结构。

实践中应该这样写:

// 推荐:工厂集中管理
GeometryFactory factory = new GeometryFactory();

// 不推荐(旧风格 / 已过时)
// Point p = new Point(new Coordinate(1, 2), factory);

3.4.4 不可变约定

JTS 的几何对象不是严格不可变的(有 apply(CoordinateFilter) 这类修改入口),但工程约定是:

  • 几何运算返回新对象,不修改输入;
  • 可以放心把输入几何缓存、复用、多线程只读共享;
  • 如果要修改几何,用 GeometryEditor 或复制一份再改(第 5 章)。

这条约定在批处理里极其重要:一个 Polygon 可以作为多个线程的只读输入,但不要在运算过程中调 apply 去改它。


3.5 双引擎架构

3.5.1 为什么会存在两套引擎

JTS 的两大核心能力——叠加(overlay)与关系判断(relate)——都经历了一次“重写换代”,且新旧两套代码同时存在:

能力 旧引擎 新引擎 新引擎引入版本
叠加 operation.overlay(OverlayOp) operation.overlayng(OverlayNG) 1.18
关系判断 operation.relate(RelateOp) operation.relateng(RelateNG) 1.20

重写的原因相同:旧引擎基于 geomgraph 的“平面图 + 节点”结构,在处理精度、自相交、共线重合等边界情况时不够鲁棒,且性能有优化空间。新引擎改用标签化的边图(OverlayNG)与基于段的拓扑计算(RelateNG),从根本上简化了状态机。

3.5.2 默认引擎是哪一个

这一点极易记错,且直接决定结果。以 1.21.0-SNAPSHOT 源码为准:

// org.locationtech.jts.geom.GeometryOverlay(包级私有类)
public static boolean OVERLAY_NG_DEFAULT = false;   // 默认仍走旧引擎

// org.locationtech.jts.geom.GeometryRelate(包级私有类)
public static boolean RELATE_NG_DEFAULT = false;    // 默认仍走旧引擎

也就是说:调用 Geometry.union(a, b)、Geometry.intersects(a, b) 等高层方法时,默认走的是旧引擎。想用新引擎,有三种方式:

// 1) 全局系统属性(启动参数,影响整个 JVM)
//    -Djts.overlay=ng -Djts.relate=ng

// 2) 显式调用新引擎 API
import org.locationtech.jts.operation.overlayng.OverlayNGRobust;
import org.locationtech.jts.operation.relateng.RelateNG;

Geometry inter = OverlayNGRobust.overlay(a, b, OverlayNG.INTERSECTION);
boolean contains = RelateNG.contains(a, b);

// 3) 在框架层统一包装(GeoTools 等下游的做法)

3.5.3 为什么新引擎不直接转正

因为 JTS 有巨量下游(GeoServer、Elasticsearch、Sedona……),任何行为变化都可能导致生产结果改变。新引擎与旧引擎在绝大多数输入上结果一致,但在“无效几何、极端坐标、精度边界”等场景可能有细微差异。这种差异对普通用户是改进,对依赖特定输出的系统可能是事故。因此 JTS 选择“先并存、后切换”的渐进策略。

对使用者的启示:排查“结果与旧版本不一致”问题时,第一件事是确认引擎开关,而不是怀疑坐标或数据。

3.5.4 引擎选择对下游的影响

场景 建议
新项目 显式使用 OverlayNGRobust / RelateNG,避开默认开关的不确定性
升级既有项目 先保持默认,跑回归;有鲁棒性问题再局部切新引擎
与 PostGIS 结果对齐 注意 PostGIS 用 GEOS,GEOS 也分批跟进新引擎,版本不同则行为不同
性能敏感 实测对比:新引擎在大规模 noding 场景通常更快,但小几何可能因初始化开销略慢

3.6 一次调用的内部旅程

3.6.1 a.intersection(b) 的调用链

以默认(旧引擎)路径为例:

Geometry.intersection(Geometry other)
  └── GeometryOverlay.intersection(a, b)
        ├── 空几何/GeometryCollection 特判
        └── SnapIfNeededOverlayOp.overlayOp(a, b, INTERSECTION)
              ├── OverlayOp.overlayOp(...)         ← 核心:geomgraph 建图、打标签
              │     ├── GeometryGraph(两个输入的平面图)
              │     ├── EdgeList / PlanarGraph
              │     ├── computeOverlay → 节点、边、面标记
              │     └── PolygonBuilder / LineBuilder / PointBuilder
              └── 失败时重试(SnapIfNeeded:加容差再试)

如果打开了 jts.overlay=ng:

Geometry.intersection(Geometry other)
  └── GeometryOverlay.intersection(a, b)
        └── OverlayNGRobust.overlay(a, b, INTERSECTION)
              ├── OverlayNG.overlay(...)            ← 核心:标签边图
              │     ├── EdgeNodingBuilder(输入边 + noder)
              │     ├── OverlayGraph(OverlayEdge 连接)
              │     ├── OverlayLabeller(Interior/Boundary/Exterior 标签)
              │     ├── PolygonBuilder / LineBuilder / OverlayMixedPoints
              │     └── PrecisionReducer / RingClipper
              └── 异常时按容差逐级重试(robust 策略)

3.6.2 一次关系判断的调用链

Geometry.intersects(Geometry other)
  └── GeometryRelate.intersects(a, b)
        └── RelateOp.intersects(a, b)          ← 默认(旧)
              ├── GeometryGraph 建图
              ├── RelateComputer.computeIM()
              └── IntersectionMatrix.isIntersects()

// 新引擎(jts.relate=ng 或显式调用)
RelateNG.relate(a, b, "T********")

3.6.3 为什么要知道调用链

因为性能优化点和 bug 定位点都在链上:

  • 想并行化?要知道 GeometryFactory 与 WKTReader 是线程安全的,但 RelateComputer 每次调用新建;
  • 想复用?要知道 PreparedGeometry 缓存的是哪一层(第 8 章);
  • 遇到 TopologyException?要知道它在 OverlayOp.computeOverlay 还是 SnapRoundingNoder 抛出。

第 9、18 章会沿这两条链深入。


3.7 坐标与序列的底层设计

3.7.1 Coordinate 家族

JTS 1.16 起把坐标拆成四个类,按“是否带 Z/M”区分:

类 含义 内存 何时用
Coordinate X、Y(旧类,仍兼容) 2 个 double 二维
CoordinateXY 仅 X、Y 2 个 double 二维、省内存
CoordinateXYM X、Y、M 3 个 double 需要测量值
CoordinateXYZM X、Y、Z、M 4 个 double 三维 + 测量
CoordinateXYZ X、Y、Z(旧) 3 个 double 三维

一个容易踩的坑:Coordinate 与 CoordinateXY 不是同一个类,混用集合会出问题;而 getZ() 在 CoordinateXY 上返回 NaN 而不是抛异常。

3.7.2 CoordinateSequence

CoordinateSequence 是坐标序列的抽象接口,提供:

  • getX(i)、getY(i)、getZ(i)、getM(i)、getCoordinate(i);
  • size()、getDimension()、getMeasures();
  • 批量操作与过滤器接口。

两个实现:

实现 特点 适用
CoordinateArraySequence 内部是 Coordinate[],灵活,Z/M 按对象类型 默认,通用
PackedCoordinateSequence 内部是 double[](或 float[]),内存紧凑、访问快 大数据量

默认工厂是 CoordinateArraySequenceFactory,不是 Packed。很多人以为 JTS 默认用紧凑数组,其实不是。要省内存需要显式指定:

GeometryFactory gf = new GeometryFactory(
    new PrecisionModel(),
    4326,
    PackedCoordinateSequenceFactory.DOUBLE_FACTORY);

第 4 章会详细对比两者的内存与性能。

3.7.3 维度、边界与位置

三个相关但容易混淆的概念:

  • Dimension(维度):0=点、1=线、2=面,用于描述几何的拓扑维度(getDimension());
  • Boundary(边界):点的边界为空,线的边界是端点,面的边界是环(getBoundary());
  • Location(位置):Interior / Boundary / Exterior 三态,是 DE-9IM 的基础(Location.INTERIOR 等)。

这三者在第 4 章与第 8 章会分别展开,这里先建立“它们不是一回事”的意识。


3.8 构建体系

3.8.1 Maven 结构

根 POM 只做两件事:聚合模块、统一版本与插件配置。核心属性:

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

这意味着所有模块产出的 class 都是 Java 8 字节码。

3.8.2 Profile 开关

-Poracle     → 构建 jts-io-ora(需要 Oracle JDBC 驱动)
-Parcsde     → 构建 jts-io-sde(需要 ArcSDE SDK,闭源)
-Dall=true   → 构建所有模块
-Drelease=true → 只构建发布产物
-DskipTests  → 跳过测试

3.8.3 质量门禁

mvn verify 会执行:

  • PMD:规则在 build-tools/src/main/resources/jts/pmd-ruleset.xml;
  • Checkstyle:规则在 build-tools/src/main/resources/jts/checkstyle.xml;
  • License header 检查:模板在 build-tools/src/main/resources/jts/header.txt,每个源文件头部必须有 EPL/EDL 声明。

这三项是 JTS 代码风格统一的基础,也是贡献 PR 时必须过的关(第 21 章)。

3.8.4 测试体系

JTS 的测试是“双轨制”:

  1. JUnit 单元测试:388 个测试类,覆盖 API、内部数据结构、算法分支;目标是 100% 覆盖率(官方 DEVELOPING.md 明示)。
  2. XML 声明式测试:129 个 XML 文件,用声明式语法描述“输入几何 + 操作 + 期望输出”,语言无关,便于跨移植项目复用(GEOS/NTS 也用同一套 XML 测试)。
modules/tests/src/test/resources/testxml/
├── general/      常规功能用例
├── validate/     有效性检查用例
├── robust/       鲁棒性/极端坐标用例
├── failure/      已知会失败的用例(回归追踪)
└── misc/         杂项

第 21 章会讲怎么写、怎么跑。


3.9 源码阅读路线

3.9.1 三条推荐路线

路线 1:从几何模型入手(适合新手)

Geometry.java
  → Point / LineString / Polygon(看 getBoundary/getDimension 实现)
  → CoordinateSequence(看坐标如何存储)
  → Envelope(看包络如何算)
  → GeometryFactory(看创建流程)

路线 2:从一次运算入手(适合排障)

Geometry.buffer(double)
  → BufferOp.bufferOp()
  → BufferCurveSetBuilder
  → OffsetCurveBuilder
  → BufferBuilder(noding + polygonize)

路线 3:从数据结构入手(适合进阶)

STRtree(索引)
  → Boundable / ItemVisitor
MCIndexNoder(节点化)
  → SegmentString / SegmentIntersector
OverlayNG(叠加)
  → OverlayEdge / OverlayLabel / OverlayGraph

3.9.2 阅读技巧

  1. 先看测试再看实现:测试里往往有最典型的输入输出,比 Javadoc 直观;
  2. 抓主路径,跳过工具方法:JTS 类里常有大量 private 辅助方法,第一遍只需看 public 主流程;
  3. 用 TestBuilder 验证猜想:读到不确定的分支,用 TestBuilder 构造输入验证;
  4. 结合 XML 测试:modules/tests/.../testxml 里有大量“输入 → 输出”的成对数据,是最好的参照;
  5. 关注 @since 注释:能快速判断某 API 是哪个版本引入的,避免用错版本。

3.9.3 常见源码“迷路”点

困惑 原因 对应位置
Geometry 里找不到实现 门面模式,实现被转发到 operation 包 Geometry 中 @see 注释
同一个功能有两份实现 双引擎并存 overlay vs overlayng
类被标记 package-private 内部实现,不对外承诺 GeometryOverlay、GeometryRelate
lab 里的类在 core 找不到 实验模块 modules/lab
文档说的方法源码里没有 文档过时或版本不符 以源码为准

3.10 源码调试与验证方法

3.10.1 用 TestBuilder 验证源码猜想

读源码时最常见的困惑是“这个分支到底什么时候走”。答案往往藏在某个输入组合里,靠盯着代码硬想效率很低。最快的办法是在 TestBuilder 里构造输入,点几次就出来了。

例如读 BufferOp 时想知道“端点样式参数在哪种几何上起作用”:

1. 打开 TestBuilder,A 输入:
   POINT (0 0)
2. 函数树选 Buffer → Buffer,参数 distance = 5
3. 结果是一个圆(顶点数由 quadrantSegments 决定)
4. 换成 LINESTRING (0 0, 10 0),观察两端是半圆
5. 在函数参数里把 capStyle 改成 FLAT,两端变成平头

这种“改一个参数、看一眼形状”的循环,比在源码里追十层调用快得多,而且能立刻建立参数与几何变化的直觉。

3.10.2 用 JtsOp 做批量对照

TestBuilder 适合单例,批量的语义验证用 JtsOp:

# 对一个文件里的所有几何执行 isValid,输出 true/false
jtsop -a data.wkt -f txt Geometry.isValid

# 对比两个版本的 union 结果
jtsop -a data.wkt -collect -f wkt Overlay.unaryUnion > union-1.20.txt

把它写进脚本,就成了一个轻量的“行为快照测试”。JTS 官方在版本升级时也大量使用这类对照。

3.10.3 在 IDE 里给 JTS 源码下断点

如果你想深入某条调用链,最有效的手段是:把 JTS 源码目录作为模块导入工程(或附加 sources jar),在关键位置下断点,用一个小 main 触发。

推荐的断点位置:

想搞清楚 断点位置
走哪个叠加引擎 GeometryOverlay.overlay()(看 isOverlayNG)
走哪个关系引擎 GeometryRelate.relate()
为什么抛 TopologyException OverlayOp.computeOverlay()、SnapRoundingNoder.computeNodes()
几何为什么无效 IsValidOp.checkValid()、PolygonTopologyAnalyzer
为什么性能差 MCIndexNoder、STRtree.query()

3.10.4 源码级“实验”

JTS 提供了 modules/example 模块,里面是一些最小示例;更实用的方式是自己在仓库里加一个临时测试类,用 mvn -pl modules/core test -Dtest=MyProbeTest 运行。这样能直接调用包级私有的类(比如 GeometryOverlay),这是外部项目做不到的。

// 放在 modules/core/src/test/java/org/locationtech/jts/geom/ 下的探针
package org.locationtech.jts.geom;

import org.junit.Test;

public class MyProbeTest {
    @Test
    public void probeOverlayDefault() {
        System.out.println("OVERLAY_NG_DEFAULT = " + GeometryOverlay.OVERLAY_NG_DEFAULT);
        System.out.println("RELATE_NG_DEFAULT  = " + GeometryRelate.RELATE_NG_DEFAULT);
    }
}

这类探针在验证“文档说的和代码做的是否一致”时特别有用——本章关于默认引擎的结论,就是这样做出来的。


3.11 模块依赖与 API 稳定性规则

3.11.1 谁能依赖谁

JTS 的模块依赖是单向的,这保证了下游可以按需裁剪:

core  ←── io/common ←── app
  ↑          ↑
  ├── tests  ┘
  ├── example
  └── lab

规则:

  1. core 不依赖任何模块(连 io 都不依赖),所以 WKT/WKB 才被放进 core 的 io 包而不是 io 模块;
  2. io 模块依赖 core,提供“非 OGC 核心格式”的读写;
  3. app/tests/lab 依赖 core(app 还依赖 io 与 tests),都是工具性质;
  4. 不存在循环依赖。

3.11.2 API 稳定性分级

范围 稳定性承诺 依据
org.locationtech.jts.*(core 公开类) 稳定,遵循语义化版本的兼容性 官方发布说明
org.locationtech.jts.io(core 内) 稳定 同上
org.locationtech.jtslab.* 不稳定,随时可能改/删 lab 模块定位
package-private 类(如 GeometryOverlay) 内部实现,不承诺 无 public 修饰
@Deprecated 标注的方法 保留但将移除 源码注释

实践建议:业务代码只依赖 public 且非 lab 的 API;需要“内部能力”时,宁可复制少量代码,也不要反射调用私有类(版本一升就崩)。

3.11.3 版本兼容性的边界

JTS 在 1.15 之后的兼容性总体很好,但有几类变化会破坏二进制兼容:

  • 新增方法(接口上加方法)——影响实现该接口的自定义类;
  • 包级私有类的重构——上游若用反射会崩;
  • 行为变化(默认引擎、精度处理)——编译通过但结果可能不同。

升级时建议的验证顺序:编译 → 单测 → 关键业务数据回归 → 性能对比。第 21 章有更详细的升级检查清单。


3.12 阅读源码的常见反模式

3.12.1 反模式一:从 Geometry.java 第一行读到最后一行

Geometry 有约 3000 行,逐行读会陷入细节。正确做法是按调用链读:挑一个你关心的公开方法(如 buffer),跟到实现类,只看主路径,遇到辅助方法先跳过。

3.12.2 反模式二:把 Javadoc 当真相

JTS 的 Javadoc 总体质量不错,但存在滞后。典型例子:

  • 某些注释还写着“使用旧算法”,而实现已经加了新路径;
  • 某些方法的 @see 指向已废弃的类;
  • 参数说明没提“空几何会怎样”。

遇到行为疑问,以源码与测试为准。本章 3.5.2 节的默认引擎结论就是靠读源码而非 Javadoc 得出的。

3.12.3 反模式三:忽略测试

JTS 的测试里有大量“输入 → 期望输出”的现成数据,尤其在 XML 测试里。想搞清一个算法的边界行为,去 testxml 里搜相关用例,比读实现快:

# 在 XML 测试里搜 buffer 相关用例
grep -rl "Buffer" modules/tests/src/test/resources/testxml/

3.12.4 反模式四:不看版本

网上大量 JTS 示例是 1.14 时代的(com.vividsolutions 包名),直接复制会编译失败。判断代码年代的方法:看 import 包名、看是否用 CoordinateXY 等新类、看是否提到 OverlayNG。

3.12.5 反模式五:只读不跑

源码里很多“看起来会失败”的分支其实有前置条件保护。只读容易误判,跑一遍(JShell、探针测试、TestBuilder)才能确认。


3.13 设计模式速查

模式 JTS 中的体现 位置
工厂 GeometryFactory、CoordinateSequenceFactory geom
访问者 GeometryFilter、ItemVisitor、CoordinateSequenceFilter geom、index
策略 Overlay 引擎切换、BufferParameters 的 join/cap 策略 operation
模板方法 GeometryTransformer、GeometryMapper geom/util
门面 Geometry 的 union/intersect/buffer geom
装饰器 PreparedGeometry 包装普通几何 geom/prep
组合 GeometryCollection 与 Multi* geom

认出这些模式后,读源码会快很多。例如看到 apply(GeometryFilter) 就知道是访问者;看到 BufferParameters 就知道是策略参数对象。


3.14 常见问题与避坑

Q1:为什么 Geometry 的构造函数是 protected? 强制用 GeometryFactory 创建,保证 SRID/PrecisionModel/CS 工厂一致。

Q2:operation.overlay 和 operation.overlayng 该用哪个? 新项目建议显式用 OverlayNGRobust;老项目保持默认,先验证再切。详见第 9 章。

Q3:为什么 GeometryFactory 要传 SRID? 只是给几何挂标签,不影响运算。传 0 表示未知。

Q4:core 真的零依赖吗? 运行时零依赖,但测试期依赖 JUnit。这一点在 mvn dependency:tree 中可以看到(scope=test)。

Q5:geomgraph 现在还有用吗? 旧 overlay/relate 引擎仍在使用它,所以不能删。新引擎不依赖它。

Q6:想扩展 JTS,应该放哪个模块? 稳定 API 放 core(需要严格测试与评审);实验性放 lab;工具类放自己的项目。第 22 章会给出扩展模板。

Q7:源码里为什么这么多 @SuppressWarnings 和 @Deprecated? 历史包袱与兼容性。看到 @Deprecated 先查 @see 指向的新 API。


3.15 本章小结

  • JTS 仓库 = build-tools + modules(core/io/app/tests/example/lab)+ doc + 一堆文档;
  • core 零运行时依赖,是它的核心竞争力之一;
  • core 有 20 个直接子包,index/operation/triangulate/linearref 等各司其职,“需求 → 包”有稳定的映射规律;
  • 几何模型是“基类 + 组合(CoordinateSequence/PrecisionModel/SRID)+ 工厂创建”,运算遵循“返回新对象、不修改输入”的约定;
  • 双引擎并存:overlay/overlayng、relate/relateng,默认开关截至 1.21 SNAPSHOT 仍是旧引擎;
  • 一次 intersection 的调用链:Geometry → GeometryOverlay → OverlayOp/OverlayNGRobust → 建图/noding → 构建结果;
  • 坐标家族有 XY/XYZ/XYM/XYZM 四种,默认序列工厂是 CoordinateArraySequenceFactory;
  • 构建用 Maven,mvn verify 跑 PMD/Checkstyle,测试是 JUnit + XML 双轨;
  • 读源码三条路线:模型入手、运算入手、数据结构入手,先看测试再看实现。

相关资源

  • JTS 仓库结构:https://github.com/locationtech/jts
  • 开发指南:https://github.com/locationtech/jts/blob/master/DEVELOPING.md
  • 技术规范(PDF):https://github.com/locationtech/jts/blob/master/doc/JTS%20Technical%20Specs.pdf
  • 开发者指南(PDF):https://github.com/locationtech/jts/blob/master/doc/JTS%20Developer%20Guide.pdf
  • 上一章:第02章 - 安装配置与快速开始
  • 下一章:第04章 - 几何对象模型详解