资讯动态

GDAL+JTS实现shp与gdb几何自相交修复及批处理实践

发布时间:2026/10/18 13:11:00 来源:尧图企业网站定制
简介面向中高级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构成多边形的环不能自相交内部环之间不能相交内环必须落在外环内部任意两个多边形不能共享边界线段。对LineStringOGC规范要求线不能有自相交端点相接的简单线除外。实际生产数据里自相交是最常见的违规类型其次是重复点、环未闭合和悬挂的小碎片多边形。自相交为什么在图形上看不出来坐标精度是关键。两个顶点相距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.OpenGDAL根据扩展名自动选择驱动。这里要强调一个容易被忽略的行为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的字段定义不会自动带过去必须手动复制。先说写gdbOGR里OpenFileGDB驱动只读另一个FileGDB驱动可以写但需要厂商SDK开源环境一般拿不到。所以我通常把修复结果写shp或GeoPackagegdb数据作为输入源如果甲方要求最终交付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。修数据不是改一两个点那么简单修完的几何要经得起质检、面积对比和后续空间分析。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑