简介:面向GIS开发人员的Java几何拓扑修复工具类,基于GDAL与JTS实现,可检测并修复几何自相交、重叠、不闭合等拓扑错误,确保数据符合OGC简单要素规范,在geotools、PostGIS等库中稳定可用。压缩包共8个文件,包含Java工具类源码、GDAL的jar依赖库,以及配套的prj、dbf、shp、sbx等示例SHP数据文件,整体大小仅169KB,结构精简,便于直接引入项目或二次修改。资源目前已有4545人学习,适合需要处理大量空间数据、进行复杂空间分析的中高级Java GIS开发者。通过其中的GdalMakeValidUtil及配套示例,开发者可快速校验和修复几何对象,降低因几何质量问题导致的空间运算失败风险,提升数据兼容性与处理效率。该工具类同时覆盖SHP与GDB两种主流格式,既适用于传统Shapefile数据,也能对接File Geodatabase,在实际GIS业务中可显著减少因数据拓扑错误引发的异常中断。
1. 自相交是怎么混进shp和gdb的:GDAL几何修复要解决什么问题
一个在 ArcMap 里看着完全正常的多边形,用ogrinfo读出再做合法性检查,经常得到 “Ring Self-intersection” 的报错。这类图形多半不是画图时画错,而是矢量化自动跟踪、坐标系投影转换、相邻图斑融合、或者不同系统之间导来导去的副产物:浮点精度在投影回 WGS84 时轻微偏移,本应共用的边界互相打了个小褶皱。更麻烦的是 shp 和 gdb 的表现不一样,shp 把非法几何原样存进文件,File Geodatabase 在构建空间索引后可能直接报几何错误,导致数据进不了后续流程。GDAL 几何修复要解决的就是这个问题:在不重画数据的前提下,把自相交环、折叠节点、无效内环重写为合法几何,同时尽量保留原坐标和属性。下面用 Java/JTS 实现一个可落地的几何拓扑修复工具类,再补上针对 shp、gdb 数据格式的 GDAL 批量处理方案。
2. 用Java和JTS做几何拓扑修复:自相交判定与修复工具类
2.1 先分清“自相交”到底指哪一类错误
JTS(Java Topology Suite)是 Java 生态里做几何运算事实上最常用的库,GDAL 底层的 GEOS 也是从 JTS 移植过去的,两边的拓扑判定语义基本一致。在 JTS 里判断一个面是否合法,直接调用Geometry.isValid(),更细的错误信息用IsValidOp拿:
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.operation.valid.IsValidOp; import org.locationtech.jts.operation.valid.ValidationError; public String diagnose(Geometry source) { if (source == null || source.isEmpty()) { return "EMPTY"; } IsValidOp op = new IsValidOp(source); ValidationError error = op.getValidationError(); if (error == null) { return "OK"; } // error.getMessage() 会给出 Ring Self-intersection、Interior is disconnected 这类原因 // error.getCoordinate() 能定位到出错坐标点,方便回原图核对 return error.getMessage() + " at " + error.getCoordinate(); }ValidationError返回的错误坐标点对排查非常有用。比如一个被裁剪算法处理过的面,自相交点往往落在某一小段折线的顶点附近,把这个点导成 shp 或 WKT 人工抽查,很快就能确认是坐标精度问题还是拓扑处理遗漏。注意这部分代码依赖org.locationtech.jts:jts-core,推荐使用 1.18 以上的版本,正常构建工具坐标如下:
<dependency> <groupId>org.locationtech.jts</groupId> <artifactId>jts-core</artifactId> <version>1.19.0</version> </dependency>自相交的错误类型和线、面有直接关系。多边形的自相交是指环在内部交叉,形状上像“8”字或“蝴蝶结”;线的自相交则是非简单线,比如一条道路上自己和自己打了个交叉。这两类的修复手段并不一样,下面分开处理。
2.2 修复策略对比:buffer(0)、union() 和 GeometryFixer
对多边形自相交,最常见的思路是 JTS 的buffer(0),也就是做一次距离为 0 的缓冲区运算。原理是缓冲区运算要求输出结果边界内所有点都到原始图形至少一个点的距离不超过缓冲距离,当距离为 0 时,算法会重新组织边界拓扑,把自相交产生的折叠带直接消化掉。这是 OGC 标准里长期保证的魔法操作,GEOS 的ST_Buffer(geom, 0)也是同样逻辑。
线自相交不能直接buffer(0),因为线缓冲成面会改变几何维度。正确做法是用union()做节点切分:union()会让相交处生成交点节点,把原来一条自交线拆成多条在交点处断开的新线段,输出仍然是线类型。工程上可根据业务决定是否保留这种切分结果,如果下游只是做长度统计,其实可以不修。
JTS 较新版本还提供了org.locationtech.jts.geom.util.GeometryFixer,它是 GEOSST_MakeValid的 Java 对应实现,对嵌套环、退化环的处理更完整。权衡下来,我的工具类默认顺序是:先手动判断几何类型做针对性修复,再用buffer(0)兜底,最后用GeometryFixer作为更高版本的加强分支。三类手段的适用面如下:
| 几何类型 | 典型非法原因 | 修复手段 | 副作用 |
|---|---|---|---|
| Polygon / MultiPolygon | 环自相交、内环外翻 | buffer(0)或GeometryFixer | 极细颈区域可能被消除 |
| LineString / MultiLineString | 非简单线、重复节点 | union() | 交点处多出节点坐标 |
| GeometryCollection | 内部混合类型 | 递归逐成员处理 | 结构可能被展平 |
2.3 一个可以直接用的GeometryTopologyFixer工具类
下面是修复工具类的核心实现,覆盖面状和线状两类自相交,输出前重新校验一次,如果仍非法会留到最后兜底逻辑处理:
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Polygon; public class GeometryTopologyFixer { private final GeometryFactory factory = new GeometryFactory(); public Geometry repair(Geometry source) { if (source == null || source.isEmpty() || source.isValid()) { return source; } Geometry candidate; if (source instanceof Polygon || source instanceof MultiPolygon) { // distance=0 的缓冲区会重建面的边界拓扑,把自交折叠消除 candidate = source.buffer(0); } else if (source instanceof LineString || source instanceof MultiLineString) { // union() 在自交点处打断线段,结果是 noded 后的线集合 candidate = source.union(); } else { candidate = source.buffer(0); } if (candidate == null || candidate.isEmpty()) { return candidate; } if (!candidate.isValid()) { // 少见的过窄折叠情况,用 GeometryFixer 再兜底一次 // JTS 1.17+ 可用;更老版本编译不过时可直接去掉这行 try { candidate = org.locationtech.jts.geom.util.GeometryFixer.fix(source); } catch (Throwable ignored) { return candidate; } } return candidate; } }调用时的参数与业务边界要说明一下。source.isValid()为 true 时直接返回原对象,避免buffer(0)给已经合法的几何增加额外节点,这对于动辄几十万条记录的 shp 很重要。buffer(0)对面积变化的影响通常极小,但遇到很窄的“领结”形图斑会发生分块,一个 Polygon 可能变成 MultiPolygon,下游如果严格要求单 Polygon,需要在读入阶段就明确要不要做-nlt CONVERT_TO_LINEAR或合并处理。线自相交用union()后,原 LineString 可能变成 MultiLineString,长度属性不受影响。
3. GDAL批量拓扑修复shp与gdb:命令与参数
3.1 先用ogrinfo做几何体检,别上来就跑修复
批量处理前我会先做一次全库体检,确认自相交要素的数量和分布。体检命令用ogrinfo加 SQLite 方言,不能直接跑在 shp 文件名上,需要先知道图层名:
# 查看shp图层名、几何类型、要素数量 ogrinfo -so -al 道路.shp # 逐要素输出合法性原因,把非法要素过滤出来 ogrinfo 道路.shp -dialect sqlite \ -sql "SELECT ST_IsValidReason(geometry) AS reason FROM 道路" -q-so -al是 summary only 加所有图层,适合先看结构;第二条命令里geometry是 shapefile 在 GDAL 内默认的几何列名,ST_IsValidReason会返回Ring Self-intersection[...]这类可读信息。如果当前 GDAL 编译版本没有集成 spatialite 函数库,第二条会报no such function,此时改用 Java 里的 JTSIsValidOp或直接跳到 3.3 节的修复方案处理。
体检的目的不只是确认有没有错,还要看非法要素占比。如果只有几十条,用 QGIS 人工定位修一下更快;如果占全库三成以上,基本可以判断是上游投影转换或矢量化参数有问题,修完数据之后还要回头改上游流程。
3.2 新版GDAL:用-makevalid一条命令修整个gdb
GDAL 3.9 以后的常见发行版在ogr2ogr里直接提供了-makevalid选项,底层调 GEOS 的 MakeValid 实现,可以直接处理 shp 和 gdb。先验证自己手上的版本支持不支持:
ogr2ogr --help 2>&1 | grep -i makevalid有输出就说明这条命令可用。shp 单文件修复:
ogr2ogr 道路_fixed.shp 道路.shp \ -makevalid \ -nlt PROMOTE_TO_MULTI \ -overwrite \ -lco ENCODING=UTF-8-makevalid自动对每个几何执行拓扑修复;-nlt PROMOTE_TO_MULTI让 Polygon 升级成 MultiPolygon 输出,防止修复后几何类型变化导致 SHP 写不进去;-lco ENCODING=UTF-8是为了让中文属性字段不乱码。gdb 整库修复更直接,不需要循环图层:
ogr2ogr -f "OpenFileGDB" 道路_fixed.gdb 道路.gdb \ -makevalid \ -nlt PROMOTE_TO_MULTI这条命令会把源 gdb 里所有图层逐一处理并写入新的 gdb。输出库名不能和输入库名相同,OpenFileGDB 驱动不会允许原地覆盖写。修复完成后ogrinfo 道路_fixed.gdb看每个图层的要素数,和修复前比对,数量变化通常来自自相交处被拆出独立图斑。
3.3 老版本GDAL:用sqlite方言走ST_MakeValid
手头 GDAL 没有-makevalid时,改用 SQLite 方言调用ST_MakeValid。注意这个函数依赖 spatialite 编译组件,如果环境里没有,还是走 Java/JTS 方案更省事。shp 的修复命令如下:
ogr2ogr 道路_fixed.shp 道路.shp \ -dialect sqlite \ -sql "SELECT ST_MakeValid(geometry) AS geometry, name, code FROM 道路" \ -nlt PROMOTE_TO_MULTI \ -overwrite \ -lco ENCODING=UTF-8SELECT里ST_MakeValid(geometry)生成修复后的几何并把它显式命名为geometry,后面列出要保留的属性字段,这里不能偷懒写*,因为*会把原始 geometry 列也带出来,输出时容易出现同名几何列冲突。属性字段列表先通过 3.1 节的ogrinfo -so -al拿到。gdb 对多图层的处理建议还是一层层跑:
ogr2ogr -f "OpenFileGDB" 道路_fixed.gdb 道路.gdb 道路 \ -dialect sqlite \ -sql "SELECT ST_MakeValid(shape) AS shape, name, code FROM 道路" \ -nlt PROMOTE_TO_MULTIgdb 的几何列名不固定,常见的是shape,具体以ogrinfo输出的字段列表为准。如果修改完想原地回写,建议先把结果输出到临时 gdb,确认无问题后再替换,不要直接在原库上尝试更新几何。
3.4 修复命令参数速查
| 参数 | 作用 | 不设置的风险 |
|---|---|---|
-makevalid | 自动修复非法几何 | 非法图形直接写入,问题保留 |
-dialect sqlite -sql | 老版本下的修复路径 | ST_MakeValid无法执行 |
-nlt PROMOTE_TO_MULTI | 几何类型提升为 Multi | Polygon 变 MultiPolygon 时写库失败 |
-lco ENCODING=UTF-8 | shp 属性编码 | 中文属性乱码 |
-overwrite | 允许覆盖输出文件 | 输出已存在时命令失败 |
4. shp与gdb格式差异下的修复参数:图层遍历与属性回填
4.1 两种格式对非法几何的“容忍度”不同
shp 本质是单个图层、一套配套文件,几何类型在一个文件里是统一的。gdb 则是一个目录容器,里面可以有多个图层、多种几何类型混存。修复时要注意的点也因此不同:shp 主要盯字段名截断和编码,gdb 主要盯图层遍历和空间参考。差异对照如下:
| 维度 | shapefile | File Geodatabase |
|---|---|---|
| 图层结构 | 一个 shp 只含一个图层 | 一个 gdb 含多个图层和多种类型 |
| 字段名长度 | 10 字符上限 | 字段名保留较长 |
| 几何列名 | 固定为 geometry | 通常为 shape,可自定义 |
| 属性编码 | 外部 .cpg / DBF 编码 | 内部 UTF-8 |
| 修复写回 | 不建议原地覆盖 | 输出库不能输入库同名 |
| 空间参考 | 无强制约束 | 常带空间参考校验 |
gdb 在读取时如果图层几何有问题,某些驱动版本会直接跳过坏要素而不是返回 null,这时候体检查到的数量可能比实际少。我的习惯是修完后再用ogrinfo重跑一遍合法性统计,两次都正常才算通过。
4.2 多图层gdb的批量拓扑修复顺序
整库修复直接用 3.2 节一条命令就能覆盖所有图层。但如果只想处理其中部分图层,比如只修面图层、保留线图层原样传递,用 Java 或脚本遍历更清晰。常见的做法是先枚举出全部图层名:
ogrinfo 道路.gdb 2>/dev/null | grep -E '^[0-9]+:' | awk -F': ' '{print $2}'输出结果按图层顺序排列,接下来逐个判断是否需要修复。处理顺序建议先修复面图层,再处理线图层,因为面图层的修复结果可能影响后续对线图层的空间关联。字段回填上,ogr2ogr默认会保留全部属性,但如果走了-sql手工指定字段列表,漏写的字段不会出现在输出中,输出前用ogrinfo -al -so做一次字段对比。
4.3 Java/GDAL绑定下的逐层质控示例
Java 直接操作 gdb 一般用 GDAL 的 Java 绑定org.gdal.ogr。下面这段代码用于逐层扫描,输出每个非法要素的图层名、FID 和错误坐标,配合前面第 2 章的 JTS 工具类做修复判断:
import org.gdal.gdal.gdal; import org.gdal.ogr.DataSource; import org.gdal.ogr.Feature; import org.gdal.ogr.Geometry; import org.gdal.ogr.Layer; import org.gdal.ogr.ogr; public class GdbTopologyScanner { public static void main(String[] args) { gdal.AllRegister(); ogr.RegisterAll(); DataSource ds = ogr.Open("道路.gdb", 0); if (ds == null) { System.err.println(ogr.GetLastErrorMsg()); return; } for (int i = 0; i < ds.GetLayerCount(); i++) { Layer layer = ds.GetLayer(i); layer.ResetReading(); Feature f; while ((f = layer.GetNextFeature()) != null) { Geometry geom = f.GetGeometryRef(); if (geom != null && !geom.IsValid()) { String wkt = geom.ExportToWkt(); System.out.println(layer.GetName() + " FID=" + f.GetFID() + " 错误几何前80字符=" + wkt.substring(0, Math.min(80, wkt.length()))); } f.delete(); } } ds.delete(); } }这段代码只做质控,不做修复,因为 GDAL 的 Java 绑定没有直接暴露 MakeValid 的便捷接口,把几何转成 WKT 再交给 JTS 是一种方案,但性能上绕一圈并不划算。实际批量修复我还是推荐 3.2 节的ogr2ogr,Java 侧负责抽样和异常定位,让 GDAL 去干重活。
4.4 修复时如何保住属性和空间参考
shp 的属性保真相对简单,默认复制所有字段,注意-lco ENCODING=UTF-8。gdb 保持空间参考比较重要:修复只改几何坐标,不重新投影,因此源库和目标库坐标系必须一致。一个很容易踩的坑是 gdb 图层本身带空间参考信息但 shp 没有.prj文件,修复后写入 gdb 时被默认坐标系接管,导致后续叠加偏位。处理前先跑:
ogrinfo -so -al 道路.gdb 2>&1 | grep -i "Extent\|Geometry:\|CRS:"确认 CRS 存在且和目标一致。如果修复结果出现坐标位置偏差,先查这个,不要去找-makevalid的麻烦。
5. 修复后怎么验证没修坏:面积基准与递归兜底
5.1 面积变化率是最快的回归指标
自相交修复最怕的不是报错,而是修复完图形变了位置。好在大部分自相交都发生在小褶皱处,修复前后总面积差值应该非常小。对修复前后的面图层做一次面积统计对比是成本最低的验证方式:
ogrinfo 道路_fixed.shp -dialect sqlite \ -sql "SELECT SUM(ST_Area(geometry)), COUNT(*) FROM 道路" -q拿这个数除以修复前的总面积,得到面积变化率。我常用的阈值是 0.5%,超过这个值说明原始图形中可能存在更严重的范围重叠或面缺边,不是单纯自相交。如果只有个别要素异常,可以用 JTS 按要素精确算:
double before = source.getArea(); double after = repaired.getArea(); double delta = Math.abs(before - after) / before;线图层不能用面积,改用长度对比,geometry.getLength()同样适用。如果修复后总长度发生明显缩短,说明自相交处被过度裁切,需要检查buffer(0)的极细颈消除副作用。
5.2 修复后仍然非法的特殊情况处理
buffer(0)和ST_MakeValid解决绝大多数情况,但偶尔会遇到输出仍然isValid() == false,集中在严重的嵌套环和 GeometryCollection 混合类型上。碰到这种要素,我会先递归展开 GeometryCollection:
public Geometry recursiveRepair(Geometry source) { if (source == null || source.isEmpty()) { return source; } if (source.getGeometryType().equals("GeometryCollection")) { GeometryFactory factory = source.getFactory(); Geometry[] parts = new Geometry[source.getNumGeometries()]; for (int i = 0; i < source.getNumGeometries(); i++) { parts[i] = recursiveRepair(source.getGeometryN(i)); } return factory.createGeometryCollection(parts); } return repair(source); }逐成员修复比整体一次buffer(0)保留的信息更多,代价是会改变原几何结构。对 gdb 里的面图层,如果递归后仍失败,最后一个保底手段是source.buffer(0.001),用极小正距离把环做一次膨胀,能强制生成合法外边界,但会引入毫米级坐标偏移,仅在验收不苛刻的情况下用。批量修复前把ogr2ogr --version和ogrinfo --formats | grep -i gdb的输出留着,环境不同导致ST_MakeValid行为不一致时,用它确认到底是谁的问题。
本文还有配套的精品资源,点击获取