第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
关键要点:
- 所有几何都继承
Geometry,共享一套元数据与通用方法(SRID、工厂、包络、维度、谓词)。 GeometryCollection既是容器又是几何;Multi*是它的特化子类。LinearRing是LineString的子类,但强制闭合(首尾坐标相同)。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 创建。原因:
- 共享配置:所有由同一工厂创建的几何共享 SRID、PrecisionModel、CoordinateSequenceFactory,避免“每个对象各配一套”的混乱;
- 保证一致性:几何运算的结果会沿用输入几何的工厂,保证 SRID 不丢;
- 便于优化:工厂可以缓存/复用内部结构。
实践中应该这样写:
// 推荐:工厂集中管理
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 的测试是“双轨制”:
- JUnit 单元测试:388 个测试类,覆盖 API、内部数据结构、算法分支;目标是 100% 覆盖率(官方
DEVELOPING.md明示)。 - 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 阅读技巧
- 先看测试再看实现:测试里往往有最典型的输入输出,比 Javadoc 直观;
- 抓主路径,跳过工具方法:JTS 类里常有大量
private辅助方法,第一遍只需看public主流程; - 用 TestBuilder 验证猜想:读到不确定的分支,用 TestBuilder 构造输入验证;
- 结合 XML 测试:
modules/tests/.../testxml里有大量“输入 → 输出”的成对数据,是最好的参照; - 关注
@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
规则:
- core 不依赖任何模块(连 io 都不依赖),所以 WKT/WKB 才被放进 core 的
io包而不是 io 模块; - io 模块依赖 core,提供“非 OGC 核心格式”的读写;
- app/tests/lab 依赖 core(app 还依赖 io 与 tests),都是工具性质;
- 不存在循环依赖。
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章 - 几何对象模型详解