简介:面向中高级GIS与Java开发者,这份几何拓扑修复工具包基于GDAL与JTS封装可直接复用的Java工具类,用于修复SHP、GDB等矢量数据中常见的自相交、重叠、不闭合等拓扑错误,确保几何对象符合OGC简单要素规范,可平稳接入GeoTools、PostGIS等空间分析栈,减少因数据非法引发的计算失败。资源共8个文件,压缩包约169KB,包含带拓扑错误的SHP样例(.shp几何、.dbf属性、.prj投影及空间索引文件)、gdalx64.jar依赖库和GdalMakeValidUtil.java核心源码,结构精简,便于对照学习和二次开发;示例数据与工具类配套,可直接运行验证修复效果,也能进一步理解GDAL与JTS在工程中的配合方式,快速沉淀一批可落地的拓扑修复方案,支撑大批量空间数据预处理与质量治理。已有4548人学习该资源。
1. GDAL几何修复到底修什么:shp/gdb里那些「看着正常」的坏图形
做地理数据处理的Java工程师,多半都遇到过这种场景:拿到的shp或gdb数据,图形表面完美,一跑质检就报几百个自相交,轻则返工,重则项目延期。所谓GDAL几何修复,就是围绕这类坏几何做拓扑层面的处理,核心是修复几何自相交,兼顾环未闭合、重复点等违规,让数据重新满足入库规则。这个需求在国土、规划类数据治理里几乎是必选项,适合要批量处理shp、gdb两种格式的Java开发者。
坏几何多的根源在数据来源混杂。CAD底图转shp时圆弧被离散成折线,手工清理节点容易拖错位,存库时看不出问题,直到叠加分析或入库质检才暴露。gdb格式更麻烦,几何经过商业平台私有存储,到开源环境里做修复,读取格式就是第一道坎。好在GDAL的Java绑定统一了shp和gdb读写,配合JTS做拓扑判断与修复,能覆盖从检测到落盘全链路。下面讲的,就是按这条链路落地时验证过可行、也踩过坑的做法。
2. 修复前的必修课:JTS的validity规则与GDAL在这条链路里的职责
2.1 isValid()背后的OGC拓扑规则:自相交到底是怎么被认定的
JTS的isValid()遵循OGC简单要素规范的规则。对Polygon来说,判定项包括:环必须闭合且点数不少于4;构成多边形的环不能自相交;内部环之间不能相交,内环必须落在外环内部;任意两个多边形不能共享边界线段。对LineString,OGC规范要求线不能有自相交,端点相接的简单线除外。实际生产数据里,自相交是最常见的违规类型,其次是重复点、环未闭合和悬挂的小碎片多边形。
自相交为什么在图形上看不出来?坐标精度是关键。两个顶点相距0.0000001度,在平面坐标下视觉上完全重合,但坐标计算判定时就会产生环的交叉。还有一种典型场景:经纬度数据跨越180度经线或者靠近极点,平面坐标系里本来连续的线会被“掰弯”,产生假自相交——这不是数据错,而是坐标系映射造成的。写判定代码时,我一般直接用IsValidOp而不是geom.isValid(),因为前者能拿到具体出错的位置和类型,批量跑日志时非常有用。
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.operation.valid.IsValidOp; import org.locationtech.jts.operation.valid.TopologyValidationError; // 不只判断是否合法,还要把错误类型和坐标记下来 public String describeInvalid(Geometry geom) { if (geom.isValid()) { return null; } // IsValidOp能给出具体的错误类型和发生位置 IsValidOp op = new IsValidOp(geom); TopologyValidationError error = op.getValidationError(); if (error != null) { // error.getErrorType()返回类型编号:自相交、环未闭合、内环在外环外等 return error.getErrorType() + " at " + error.getCoordinate(); } return "unknown"; }这段代码的逻辑说明:IsValidOp的构造会立即执行有效性分析,不依赖Geometry.isValid()内部可能存在的缓存分支。TopologyValidationError携带错误类型编号和出错坐标,拿到类型码之后可以按项目需要做分类统计,比如统计某批shp里自相交占比多少、环未闭合占比多少,给上游生产环节提供整改依据。参数上要注意,Coordinate是原始数据坐标,如果怀疑是坐标精度导致的假自相交,可以把这个坐标和相邻点一起打出来看距离,距离小于1e-8就按精度问题处理,而不是当真实交叉去修改。
2.2 为什么用JTS做修复、GDAL只负责格式读写
这条链路的选型理由其实很明确。GDAL的OGR模块确实有自己的几何修复能力,但在Java工程里,GDAL的Java绑定在几何操作上的完整度和生态都不如JTS。JTS有buffer(0)这种经典的拓扑修复手段,有专门做节点级修复的GeometryFixer,还有完整的validity检测和精度控制工具,纯Java实现,调试和单测都方便。GDAL在这条链路里的定位就是格式层:shp、gdb的读取和写回都由它负责,几何计算全部交给JTS。这种分工避免了“用GDAL写拓扑算法”这种拧巴的用法。
另外一个重要理由是依赖可控性。GDAL是本地库,Java绑定跨平台要带一堆动态库,版本冲突的排查成本很高;JTS是纯Java,风险低。所以常见的落地结构是:GDAL只做最外层的数据源访问,读取要素后立刻把几何转成JTS对象,修复完成后再转回GDAL写入。这个转换要稳定且不丢精度,最可靠的中转格式是WKB,而不是GeoJSON——GeoJSON会经历一次文本解析,坐标精度在小数位数上容易出问题;WKB是二进制,坐标按双精度直接读写,精度无损。
import org.gdal.ogr.Geometry; // GDAL的几何类 import org.locationtech.jts.io.WKBReader; import org.locationtech.jts.io.WKBWriter; // GDAL几何转JTS几何:通过WKB交换,坐标按双精度读写,不丢位数 public static org.locationtech.jts.geom.Geometry toJts(Geometry ogrGeom) { if (ogrGeom == null) { return null; } byte[] wkb = ogrGeom.ExportToWkb(); // 导出到内存,注意它用的是机器字节序 try { return new WKBReader().read(wkb); } catch (Exception e) { throw new IllegalArgumentException("几何转JTS失败,可能不是合法WKB", e); } } // JTS几何转回GDAL几何,修复后写盘前调用 public static Geometry toOgr(org.locationtech.jts.geom.Geometry jtsGeom) { byte[] wkb = new WKBWriter().write(jtsGeom); return Geometry.CreateFromWkb(wkb); }这里有一个容易翻车的点:GDAL的ExportToWkb()返回机器字节序,不同机器可能是大端也可能是小端,而WKBWriter默认写大端。好在JTS的WKBReader内部会自动探测字节序,GDAL读入也会自动判断,所以这两步转换通常不会出错;但如果读者自己写WKB解析,一定记得处理字节序,否则多边形坐标会出现天文数字。另外,Geometry.CreateFromWkb返回的org.gdal.ogr.Geometry对象,用完要记得delete(),GDAL本地对象在finalize之外最好手动释放,否则长任务里内存只涨不降。
3. 把修复工具类写出来:shp和gdb的读取、自相交修复、写回一整套
3.1 用GDAL的Java绑定读取shp与gdb的几何和属性
读取这步的代码模式其实是统一的,shp和gdb都走ogr.Open,GDAL根据扩展名自动选择驱动。这里要强调一个容易被忽略的行为:ogr.Open失败返回null而不是抛异常,新手常在这地方踩坑,null继续往下走就是空指针,所以第一件事就是判空,并把错误原因打出来。
import org.gdal.gdal.gdal; import org.gdal.ogr.*; // 注册全部驱动,必须在任何Open之前执行一次 gdal.AllRegister(); ogr.RegisterAll(); // 支持.shp和.gdb目录,统一入口 public static DataSource openInput(String path) { // 第二个参数0表示只读;Open是静态方法,失败返回null DataSource ds = ogr.Open(path, 0); if (ds == null) { // gdal.GetLastErrorMsg()能拿到驱动加载失败的原始信息 throw new IllegalStateException("无法打开数据源: " + path + ", 错误: " + gdal.GetLastErrorMsg()); } return ds; } // 逐要素流式读取,避免大图全部载入内存 public static void forEachFeature(DataSource ds, FeatureCallback cb) { Layer layer = ds.GetLayerByIndex(0); // 生产数据第一个图层通常就是目标 Feature feature; while ((feature = layer.GetNextFeature()) != null) { try { // GetGeometryRef返回的几何是内部引用,转换后可以放心持有 Geometry ogrGeom = feature.GetGeometryRef(); cb.handle(feature, ogrGeom); } finally { feature.delete(); // 手动释放本地对象,长任务的关键 } } }逻辑说明:gdal.AllRegister和ogr.RegisterAll是两步注册,OGR模块的驱动必须注册过才能识别gdb这类私有格式;OpenFileGDB驱动默认被注册,但它是只读的。GetLayerByIndex通常取第一个图层,如果gdb里有多个图层,更稳的做法是GetLayerByName并传入图层名,所以工具类应该支持图层名参数。feature.delete()这行容易被忽略,GDAL的Feature在Java里持有本地指针,GC管不到,大批量循环不主动删除,几个小时的任务跑到一半就会内存溢出,这个坑在下一章专门展开。
3.2 自相交修复核心方法:isValid判断、buffer(0)、GeometryFixer
修复方法的设计原则是“先检测后修复,优先保守修复,最后才上魔法buffer(0)”。这条顺序有讲究。GeometryFixer是JTS加入的专门修复工具,它会在尽量少改动坐标的前提下调整环的结构,把自相交消除掉;而buffer(0)虽然能修几乎所有Polygon的拓扑问题,但它本质上是做一次零距离缓冲区运算,会在顶点处引入圆角线段,让多边形边界的形状发生肉眼可见的细微变化,对面积敏感的质检很致命。所以核心方法里,GeometryFixer是首选,buffer(0)只做兜底。
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.util.GeometryFixer; public static Geometry repairGeometry(Geometry geom) { if (geom == null || geom.isEmpty()) { return geom; } // 1. 先检测,合法就直接返回,避免无谓的几何运算 if (geom.isValid()) { return geom; } // 2. 用GeometryFixer做节点级修复,尽量保留原坐标 Geometry fixed = GeometryFixer.fix(geom); // 3. 如果修复结果仍不合法,说明环结构被破坏得比较严重, // 退回buffer(0),让缓冲区算法重新构建合法边界 if (!fixed.isValid()) { fixed = geom.buffer(0); } // 4. 修完以后清理修复过程产生的重复坐标点 return fixed.removeRepeatedPoints(); }逻辑说明:检查顺序是双保险,第一次isValid让大部分合法数据直接通过,避免大量无意义的buffer计算;第二次isValid用于识别GeometryFixer处理不了的极端情况。buffer(0)的数学原理不需要深究,只需要知道它是对几何做半径为零的缓冲区运算,JTS内部会用noding算法重新切割边界,自相交的环在这个过程会被拆开或规范化,最终产出一个合法多边形。参数上要理解removeRepeatedPoints只移除完全重复的连续坐标点,不会改变几何形状,如果原始数据精确到13位小数,修复后的坐标数量会变少,这属于正常优化。
3.3 写回shp与gdb:驱动选择与字段保留策略
写回阶段有两个容易出问题的点:第一,gdb写入受限;第二,shp的字段定义不会自动带过去,必须手动复制。先说写gdb,OGR里OpenFileGDB驱动只读,另一个FileGDB驱动可以写但需要厂商SDK,开源环境一般拿不到。所以我通常把修复结果写shp或GeoPackage,gdb数据作为输入源;如果甲方要求最终交付gdb,常见做法是拿修复后的shp在商业GIS软件里做一次导入,这步用脚本自动化即可。
注意:判断驱动是否可写,不要靠猜,用TestCapability(ogr.ODrCCreateDataSource)在代码层面确认。
import org.gdal.ogr.ogr; import org.gdal.ogr.Driver; // 判断驱动是否支持创建数据源,避免写盘时才发现是只读的 public static boolean isWritable(String driverName) { Driver drv = ogr.GetDriverByName(driverName); return drv != null && drv.TestCapability(ogr.ODrCCreateDataSource); } // 输出shp到指定目录,目录不需要预先存在,CreateDataSource会创建 public static void writeShp(DataSource inputDs, String outDir) { Driver shpDriver = ogr.GetDriverByName("ESRI Shapefile"); // 驱动注册名是固定字符串 if (shpDriver == null || !shpDriver.TestCapability(ogr.ODrCCreateDataSource)) { throw new IllegalStateException("当前环境没有可用的Shapefile写驱动"); } DataSource outDs = shpDriver.CreateDataSource(outDir); Layer inLayer = inputDs.GetLayerByIndex(0); // 保持坐标系一致,投影信息不能丢 Layer outLayer = outDs.CreateLayer("fixed", inLayer.GetSpatialRef(), ogr.wkbPolygon); copyFields(inLayer, outLayer); // 逐要素写回,属性从原要素拷贝,几何用修复后的WKB构造 Feature inFeature; while ((inFeature = inLayer.GetNextFeature()) != null) { Feature outFeature = new Feature(outLayer.GetLayerDefn()); for (int i = 0; i < inLayer.GetLayerDefn().GetFieldCount(); i++) { String fieldName = inLayer.GetLayerDefn().GetFieldDefn(i).GetName(); outFeature.SetField(fieldName, inFeature.GetFieldAsString(fieldName)); } // 修复后的几何通过WKB转回OGR,再绑定到输出要素 byte[] fixedWkb = new WKBWriter().write(repairGeometry(toJts(inFeature.GetGeometryRef()))); outFeature.SetGeometry(Geometry.CreateFromWkb(fixedWkb)); outLayer.CreateFeature(outFeature); inFeature.delete(); outFeature.delete(); } } // 字段定义完整复制,类型和宽度一起带过去 public static void copyFields(Layer inLayer, Layer outLayer) { FeatureDefn defn = inLayer.GetLayerDefn(); for (int i = 0; i < defn.GetFieldCount(); i++) { FieldDefn fd = defn.GetFieldDefn(i); outLayer.CreateField(fd); } }逻辑说明:字段复制必须显式做,GDAL不会自动把输入图层的LayerDefn克隆到输出图层。SetField以字段名作为参数,字段类型兼容性问题在shp里比较常见,比如宽字符串字段被截断、日期字段精度丢失,这些是格式本身的限制,只能在拷贝前检查FieldDefn的字段类型并做对应处理。参数说明:CreateLayer的第三个参数是几何类型,用ogr.wkbPolygon就行,但如果数据里混入了线和点,这里最好用ogr.wkbUnknown,避免写入时几何类型不匹配报错。写回后属性为空,多半是只SetGeometry没有逐个赋值属性,所以属性拷贝和几何赋值缺一不可。
4. 拓扑修复避坑指南:gdb写入、字段丢失、内存溢出五个现实坑
4.1 gdb写入总是失败:CreateDataSource返回null,连报错都没有
现象:代码里对目标.gdb目录调用CreateDataSource,返回的结果是null,程序继续往下走就直接空指针崩溃;更麻烦的是gdal.GetLastErrorMsg此时也没给出有效信息,日志里只有一行NullPointerException,定位半天也找不到根因。
原因:绝大多数环境里的gdb支持来自OpenFileGDB驱动,这个驱动只实现了读取路径,根本没有创建数据源的能力;另一个FileGDB驱动虽然完整支持读写,但需要厂商SDK,开源版GDAL的Java绑定不携带它。CreateDataSource在这种组合下只能安静地返回null,而不是抛异常。
解决:动手写之前先调用TestCapability(ogr.ODrCCreateDataSource)做可写性检查,把“能不能写”提前暴露。生产项目里我习惯把gdb作为输入源,修复结果导出shp或GeoPackage,最后用商业GIS软件的导入功能转回gdb,这一步能写成命令行脚本并放进数据治理流水线自动化执行。别在流程中途才尝试写gdb,翻车概率极高。
4.2 buffer(0)修完自相交,面积却变了:质检死活过不去
现象:自相交修好了,isValid()返回true,但质检报告显示面积和原始数据差了一截,抽查图形发现角点处出现了细微的圆弧。
原因:buffer(0)不是“改一两个顶点”的修复,它是完整的缓冲区运算。JTS在顶点处用圆弧拟合角点,即使半径为零,输出的边界仍然可能包含弧段坐标,面积自然变化。
解决:优先用GeometryFixer而不是buffer(0),前者是节点级修复,会尽量保留原坐标。只有GeometryFixer反复修不出合法结果,才退回buffer(0)。如果必须用buffer(0),修复后要测面积变化率,超过阈值就报警人工介入。我在某跨平台系统里定过5%的阈值,超过就把要素的fid打进日志,抽查时直接定位到具体图形。
4.3 修完写出来字段全丢了:只剩一个OBJECTID
现象:输出shp打开,属性表里只有一个默认的OBJECTID字段,原始数据的图层名、地类代码、权属信息全不见了。
原因:CreateLayer只创建了几何图层,不会继承输入图层的字段定义;shp格式对字段名还有10字符截断限制,字段类型转换也可能失败。
解决:写回前遍历输入LayerDefn,用字段名、类型、宽度逐项CreateField复制到输出图层。还要注意shp的字段名截断问题,工程上一般提前约定好输出字段名映射表。字段定义复制用上一章给出的copyFields方法,FieldDefn是GDAL里字段定义的完整对象,包含名称、类型、宽度、精度,CreateField直接接收它是最省事的复制方式。有人图省事用SetField循环,遇到非字符串类型会静默转换出错,属性变成null,这类问题在质检时才会暴露,排查代价很高。
4.4 几十万要素跑到一半内存溢出:循环里没有delete
现象:处理一个几十万要素的gdb,跑了半小时内存持续上涨,最终OOM崩溃,日志里没有任何异常,因为堆直接被顶爆。
原因:GDAL的Feature、Geometry是本地对象,Java侧拿到的是引用,GC不感知这些引用背后的内存。循环里new了大量Feature但不delete,本地内存持续累积。
解决:每处理完一个要素,手动调用feature.delete()。我习惯在finally块里做释放,保证异常分支也不会泄漏。另外,尽量不要把整个图层的几何收集到List里统一修复,正确姿势是流式处理:读一个,修一个,写一个,释放一个。这个模式在forEachFeature里已经体现,批量处理时务必遵循。内存翻车不值得,释放本地对象就在一行代码。
4.5 明明是自相交错误,放大看却干干净净:坐标精度与假自相交
现象:isValid()报Self-intersection,但把几何放大到最大比例尺,肉眼根本看不到任何交叉。
原因:两种最常见的情况。一是坐标精度问题,两个顶点相距极小,在平面坐标上看起来重合,但计算时产生交叉。二是经纬度数据跨越180度经线,平面坐标系要把连续的线“切开”,切开处就会出现视觉上的交叉,这是投影映射造成的假自相交,不是数据真错。
解决:先去重复点再用IsValidOp判断,很多误报会消失;跨经线的数据处理前先做坐标预处理,比如将经纬度转成投影坐标系再修复,修完转回来。代码上判断“是不是真自相交”可以打印出错点与相邻点的距离,距离小于1e-8就按精度问题处理。这个区分很重要,否则会把合法数据修坏,修坏的数据再想回到原样就没有后悔药了。
5. 参数调优与质检:修复到什么程度才算数
5.1 buffer(0)的容差陷阱:经纬度与投影坐标系下的行为差异
很多人以为buffer(0)的“0”就是完全精确,实际不是。JTS的几何运算发生在平面笛卡尔坐标空间,它不感知坐标系单位。同样的自相交,在投影坐标系下用buffer(0)修复,误差在毫米级别;在经纬度下,误差被换算成度的小数位,容易超出精度容忍范围。比如1e-9度在赤道附近大约对应0.1毫米,但到高纬度地区,经度方向的实际距离被严重压缩,同样的1e-9度对应的地面距离小得多。这导致同一个算法在不同区域的数据上表现不稳定。
解决思路:遇到经纬度数据,先统一转换为投影坐标系做修复,完成后再转回WGS84输出。如果项目要求全程经纬度,也可以先做一次坐标量化,用GeometryPrecisionReducer把坐标归一到固定小数位,把“不可控的浮点误差”变成“可控的坐标精度”。
import org.locationtech.jts.geom.PrecisionModel; import org.locationtech.jts.precision.GeometryPrecisionReducer; // 量化到6位小数,约等于米级精度,适合经纬度修复前预处理 public static org.locationtech.jts.geom.Geometry quantize(org.locationtech.jts.geom.Geometry geom) { PrecisionModel pm = new PrecisionModel(1_000_000); return GeometryPrecisionReducer.reduce(geom, pm); }注意:reduce要在修复前调用,顺序反了会把修复结果重新量化,产生新的自相交。参数上,PrecisionModel(1_000_000)表示保留6位小数;如果数据是投影坐标,建议用new PrecisionModel(1000)把坐标量化到毫米级。这个参数没有通用最优值,要看质检要求的坐标容差,先量化后修复,能让结果稳定很多。
5.2 二次质检与修复率统计:用报告证明修复有效
修复完之后不能直接交付,至少要跑一遍全量质检并把统计结果写进报告。统计口径常见做法是:total表示输入要素总数,validCount表示本来就合法的要素数,fixedCount表示修复后合法且面积变化在容差内的要素数,修复率 = (validCount + fixedCount) / total。这个修复率是交付数据时最有说服力的指标。
我还会在质检环节做交叉验证:把修复后的shp用GDAL命令行工具再做一次拓扑检查,如果命令行的检查结果和JTS的统计结果不一致,说明两种实现的有效性判定有差异,需要人工抽几个要素出来看。这个交叉验证能发现很多JTS判定和OGR判定之间的边界情况。
// 修复率统计与面积异常告警 int total = 0, validCount = 0, fixedCount = 0; for (Feature f : features) { Geometry geom = toJts(f.GetGeometryRef()); total++; if (geom.isValid()) { validCount++; continue; } Geometry fixed = repairGeometry(geom); if (fixed.isValid()) { fixedCount++; // 面积变化超过5%的记录fid,标记为待人工复核 double delta = Math.abs(geom.getArea() - fixed.getArea()); if (geom.getArea() > 0 && delta / geom.getArea() > 0.05) { logger.warn("面积变化超过5%, fid={}", f.GetFID()); } } } double repairRate = (validCount + fixedCount) / (double) total * 100; logger.info("修复率={}%, 共{}个要素", repairRate, total);说明:这个统计循环和修复循环可以合并,但工程上我倾向分开跑,先修一遍落盘,再用独立的统计代码做质检,避免统计逻辑影响修复流程。参数上5%的面积变化阈值是我在多个实际项目中试出来的经验值,对权属类数据比较合适;对高精度地形数据,阈值要收紧到1%甚至更低。GetFID返回的是要素在文件里的序号,正好作为人工复核的定位依据。
5.3 性能边界:十万级要素的批处理怎么跑不翻车
批处理性能取决于几何复杂度而不是要素数量,我的经验数据放在下表供参考。
| 数据规模 | 推荐模式 | 经验表现 |
|---|---|---|
| 十万要素以内 | 单线程流式读取+修复 | 视几何复杂度,一般可在几分钟内完成 |
| 十万到百万 | 按fid分片多线程,每个线程独立打开DataSource | 吞吐能提升3到5倍,但要注意驱动线程安全 |
| 百万以上 | 先按空间索引分块,逐块处理再合并 | 需要分块边界去重,建议做单独处理 |
多线程处理有几个硬性约束:JTS的GeometryFactory可以跨线程共享,但WKBReader和WKBWriter不是线程安全的,每个线程要new一个;GDAL的DataSource不能多线程同时遍历同一个图层,稳妥做法是每个线程独立打开数据源并按fid范围取数。曾有人把单个DataSource塞进线程池,结果读出来的几何出现错乱,查了很久才发现是驱动本地状态串了。
分块处理时注意:空间分块是按包围盒切数据,落在边界上的要素可能同时出现在两个块里,合并时要去重。我的做法是每个块输出独立shp,最后用追加模式合并,合并后重新拓扑检查一遍,保证跨块边界没有引入新问题。还有一个实用习惯:修完一个要素就写一个,不要攒批,写盘开销远小于修复计算,攒批反而增加内存压力和崩溃恢复成本。
6. 压箱底技巧:按几何类型分策略修复,结果更稳
我踩过最深的坑是把所有几何都往buffer(0)里扔。某次管线数据里混着几千条线要素,buffer(0)把每条线变成了细长多边形,属性没变,但几何类型变了,后续分析全崩。所以这个工具类从那时起就改成按几何类型分策略:面要素走GeometryFixer加buffer(0)兜底,线要素用noding而不是buffer,集合要素递归处理。
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.Polygon; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.operation.union.UnaryUnionOp; public static Geometry repairByType(Geometry g) { if (g == null || g.isEmpty()) { return g; } if (g instanceof Polygon || g instanceof MultiPolygon) { // 面:GeometryFixer优先,buffer(0)兜底 return repairGeometry(g); } if (g instanceof LineString || g instanceof MultiLineString) { // 线:用UnaryUnionOp做noding,修自相交的同时保持线的类型不变 return UnaryUnionOp.union(g); } // GeometryCollection或混合几何:逐子几何修复再重组 GeometryFactory gf = g.getFactory(); Geometry[] parts = new Geometry[g.getNumGeometries()]; for (int i = 0; i < g.getNumGeometries(); i++) { parts[i] = repairByType(g.getGeometryN(i)); } return gf.buildGeometry(java.util.Arrays.asList(parts)); }UnaryUnionOp对线要素做的是节点化处理,把自相交的线在交点处断开再重组,类型和维度都不会变,这是线修复的正确姿势。面要素不要用UnaryUnionOp,因为面还有内外环关系,union会把洞填掉,得不偿失。
这套方法现在是我所有几何处理工具类的地基。从那以后,凡是涉及矢量修复的需求,我第一件事就是先问清数据类型和坐标系,再决定修复策略,而不是拿到几何就无脑buffer。修数据不是改一两个点那么简单,修完的几何要经得起质检、面积对比和后续空间分析。希望帮到你。
本文还有配套的精品资源,点击获取