如果需要详细的地图应用步骤可参考博主的另一篇分割文章https://blog.csdn.net/qq_47285211/article/details/159544070?spm1011.2124.3001.6209在 GIS 前端开发、国土空间规划、图斑编辑等场景中多边形修整是核心刚需功能 —— 类似 ArcGIS 的修整工具通过一条线切割多边形自动保留面积更大的有效图斑剔除细碎小面。原生 MapboxGL 没有直接提供高精度、可控的多边形修整 API而简单调用 Turf.js 的difference、union会出现坐标丢失、自相交、空洞错乱、碎面残留等问题无法满足生产环境要求。本文基于Turf.js封装了一套高精度、兼容生产、逻辑对标 ArcGIS的多边形修整类支持完全保留原始坐标不简化、不偏移正确处理多边形空洞内环归属自动识别有效交点拒绝无效切割切割后自动保留面积大的图斑一、功能核心亮点高精度几何处理1:1 复刻分割工具坐标逻辑容差1e-8统一对齐仿 ArcGIS 修整逻辑画线切割 → 生成两面 → 保留大面空洞智能分配按质心归属 距离判断保证空洞不丢失、不错位健壮异常处理过滤无效交点、重复点、极小面、自相交生产可用返回标准 GeoJSON可直接用于 Mapbox 渲染 / 后端入库无侵入、纯前端仅依赖 Turf.js可直接集成 Vue/React/ 原生项目二、适用场景征地范围边界调整规划图形裁剪优化前端 GIS 图形编辑工具与 MapboxGL Draw 结合实现交互式图斑编辑三、完整实现代码可直接复制使用import * as turf from turf/turf; class PolygonTrimmer { constructor() { this.turf turf; this.EPS 1e-8; // 坐标精度容差和分割工具一致 this.tolerance 1e-10; // 浮点判断精度 } /** * 核心方法修整多边形模拟ArcGIS修整工具- 生成两个候选面后保留面积大的 * param {Object} polygonGeoJSON - 原始多边形 GeoJSON (Polygon/MultiPolygon) * param {Object} lineGeoJSON - 修整线 GeoJSON (LineString) * returns {Object|null} 修整后的多边形 GeoJSON面积大的那个失败返回null */ trimPolygon(polygonGeoJSON, lineGeoJSON) { try { // 1. 基础校验和分割工具保持一致 if (!polygonGeoJSON) throw new Error(未传入目标多边形); if (!lineGeoJSON) throw new Error(未传入修整线); // 统一转为单Polygon处理 const polygonFeature this._normalizeToPolygon(polygonGeoJSON); const lineFeature this.turf.feature(lineGeoJSON.geometry); if (lineFeature.geometry.type ! LineString) { console.error(仅支持LineString类型的修整线); return null; } // 2. 提取多边形外环和空洞 const originalOuterRing polygonFeature.geometry.coordinates[0]; const originalHoles polygonFeature.geometry.coordinates.slice(1); const polygonBoundary this.turf.lineString(originalOuterRing); // 转为边界线 // 3. 查找修整线与多边形边界的有效交点对参考分割工具逻辑 const validIntersections this._findValidIntersectionPair(polygonBoundary, lineFeature); if (!validIntersections || validIntersections.length 2) { console.warn(修整线必须与多边形边界相交两次); return null; } const [p1, p2] validIntersections; console.log(有效交点对, p1, p2); console.log(交点距离, this.turf.distance(this.turf.point(p1), this.turf.point(p2))); console.log(交点1是否在多边形边界, this._isPointOnSegment(p1, originalOuterRing[0], originalOuterRing[1])); // 4. 手动切割外环100%保留原始坐标参考分割工具_manualSplitRing const [clippedRing1, clippedRing2] this._manualSplitRing(originalOuterRing, p1, p2); // 5. 手动切割修整线仅保留交点间的线段 const clippedTrimLineCoords this._manualSplitLine(lineFeature.geometry.coordinates, p1, p2); // 6. 拼接两个候选面的外环参考分割工具_connectLine const poly1Outer this._connectRingAndLine(clippedRing1, clippedTrimLineCoords); const poly2Outer this._connectRingAndLine(clippedRing2, clippedTrimLineCoords); // 7. 处理空洞归属参考分割工具的空洞逻辑 const { poly1Holes, poly2Holes } this._assignHolesToPolygons(originalHoles, poly1Outer, poly2Outer); // 8. 构建两个完整的候选多边形 const candidateA this.turf.polygon([poly1Outer, ...poly1Holes], polygonFeature.properties); const candidateB this.turf.polygon([poly2Outer, ...poly2Holes], polygonFeature.properties); // 9. 清理坐标修复自相交保留原始精度 const cleanedA this._cleanPolygon(candidateA); const cleanedB this._cleanPolygon(candidateB); // 10. 面积对比保留大面、移除小面 const areaA this.turf.area(cleanedA); const areaB this.turf.area(cleanedB); const minValidArea 0.0001; // 极小面过滤阈值 // 校验有效面积 if (areaA minValidArea areaB minValidArea) { console.error(两个候选面面积均过小修整失败); return null; } // 选择面积大的面作为最终结果 let finalResult; if (areaA areaB) { finalResult cleanedA; console.log(保留候选面A面积${areaA.toFixed(6)}移除候选面B面积${areaB.toFixed(6)}); } else { finalResult cleanedB; console.log(保留候选面B面积${areaB.toFixed(6)}移除候选面A面积${areaA.toFixed(6)}); } // 补充属性参考分割工具的属性规则 finalResult.properties { ...polygonFeature.properties, trim: true, trimTime: Date.now(), trimId: ${polygonFeature.properties.gid || poly}_trim_0, candidateAreaA: areaA, candidateAreaB: areaB, selectedByArea: true, selectedCandidate: areaA areaB ? A : B, innerRingsCount: finalResult.geometry.coordinates.length - 1, originalInnerRingsCount: originalHoles.length }; const originalArea this.turf.area(polygonFeature); const finalArea this.turf.area(finalResult); console.log(原始面积, originalArea.toFixed(6)); console.log(修整后面积, finalArea.toFixed(6)); // 检查几何是否一致 const isSameGeometry JSON.stringify(finalResult.geometry.coordinates) JSON.stringify(polygonFeature.geometry.coordinates); if (isSameGeometry) { console.warn(修整后几何与原始几何一致可能是修整线无效); } return finalResult; } catch (error) { console.error(修整多边形失败:, error); return null; } } /** * 归一化为单Polygon处理MultiPolygon * private */ _normalizeToPolygon(geoJSON) { const feature this.turf.feature(geoJSON.geometry); if (feature.geometry.type MultiPolygon) { // 取第一个Polygon和分割工具逻辑一致 return this.turf.polygon(feature.geometry.coordinates[0], feature.properties); } else if (feature.geometry.type Polygon) { return feature; } else { throw new Error(仅支持Polygon/MultiPolygon类型当前为${feature.geometry.type}); } } /** * 查找修整线与多边形边界的有效交点对参考分割工具_setFloors * private */ _findValidIntersectionPair(boundaryLine, trimLine) { const intersections this.turf.lineIntersect(boundaryLine, trimLine).features; if (intersections.length 2) return null; // 1. 过滤无效交点必须在边界线段上非顶点 const validPoints []; for (const p of intersections) { const pt p.geometry.coordinates; let isOnSegment false; // 检查是否在边界线段上非顶点 for (let i 0; i boundaryLine.geometry.coordinates.length - 1; i) { const a boundaryLine.geometry.coordinates[i]; const b boundaryLine.geometry.coordinates[i 1]; if (this._isPointOnSegment(pt, a, b) this.turf.distance(this.turf.point(pt), this.turf.point(a)) this.EPS this.turf.distance(this.turf.point(pt), this.turf.point(b)) this.EPS) { isOnSegment true; break; } } if (isOnSegment) validPoints.push(pt); } if (validPoints.length 2) return null; // 2. 去重距离 EPS const uniquePoints []; for (const pt of validPoints) { const isDuplicate uniquePoints.some(u this.turf.distance(this.turf.point(u), this.turf.point(pt)) this.EPS ); if (!isDuplicate) uniquePoints.push(pt); } if (uniquePoints.length 2) return null; // 3. 按修整线方向排序 uniquePoints.sort((a, b) { const distA this.turf.nearestPointOnLine(trimLine, this.turf.point(a)).properties.location; const distB this.turf.nearestPointOnLine(trimLine, this.turf.point(b)).properties.location; return distA - distB; }); return uniquePoints.slice(0, 2); } /** * 手动切割外环100%保留原始坐标参考分割工具_manualSplitRing * private */ _manualSplitRing(ringCoords, p1, p2) { // 移除闭合点保留原始坐标 const ring [...ringCoords.slice(0, -1)]; // 修复遍历到倒数第一个点i ring.length - 1 const insertPoint (pt) { // 先检查是否已存在该点 for (let i 0; i ring.length; i) { if (this.turf.distance(this.turf.point(ring[i]), this.turf.point(pt)) this.EPS) { return; // 已存在无需插入 } } // 插入到线段中间 for (let i 0; i ring.length - 1; i) { const a ring[i], b ring[i 1]; if (this._isPointOnSegment(pt, a, b)) { ring.splice(i 1, 0, pt); return; } } // 无有效线段返回null切割失败 throw new Error(交点 ${pt} 不在多边形外环线段上); }; try { insertPoint(p1); insertPoint(p2); } catch (e) { console.error(插入交点失败, e); return [ringCoords, ringCoords]; // 返回原环避免报错 } // 查找交点索引 const idx1 this._findPointIndex(ring, p1); const idx2 this._findPointIndex(ring, p2); if (idx1 -1 || idx2 -1 || idx1 idx2) { console.warn(交点索引无效切割失败); return [ringCoords, ringCoords]; } // 切割为两段 let part1, part2; if (idx1 idx2) { part1 ring.slice(idx1, idx2 1); part2 ring.slice(idx2).concat(ring.slice(0, idx1 1)); } else { part1 ring.slice(idx2, idx1 1); part2 ring.slice(idx1).concat(ring.slice(0, idx2 1)); } // 闭合环确保首尾一致 part1 [...part1, part1[0]]; part2 [...part2, part2[0]]; // 验证切割结果 console.log(切割后环1长度, part1.length); console.log(切割后环2长度, part2.length); return [part1, part2]; } /** * 手动切割修整线参考分割工具_manualSplitLine * private */ _manualSplitLine(lineCoords, p1, p2) { const line [...lineCoords]; // 插入交点到线中 const insertPoint (pt) { for (let i 0; i line.length; i) { const a line[i], b line[i 1]; if (this._isPointOnSegment(pt, a, b)) { line.splice(i 1, 0, pt); return; } } line.push(pt); }; insertPoint(p1); insertPoint(p2); // 查找交点索引并切割 const idx1 this._findPointIndex(line, p1); const idx2 this._findPointIndex(line, p2); return idx1 idx2 ? line.slice(idx1, idx2 1) : line.slice(idx2, idx1 1); } /** * 拼接环和修整线参考分割工具_connectLine * private */ _connectRingAndLine(ring, lineCoords) { if (lineCoords.length 2) return ring; // 空修整线返回原环 const ringEnd ring[ring.length - 2]; // 倒数第二个点排除闭合点 const lineStart lineCoords[0]; const lineEnd lineCoords[lineCoords.length - 1]; // 修复增加距离阈值避免精度误差 const distToStart this.turf.distance(this.turf.point(ringEnd), this.turf.point(lineStart)); const distToEnd this.turf.distance(this.turf.point(ringEnd), this.turf.point(lineEnd)); let mergedLine; if (distToStart this.EPS * 10) { mergedLine [...ring.slice(0, -1), ...lineCoords.slice(1)]; } else if (distToEnd this.EPS * 10) { mergedLine [...ring.slice(0, -1), ...lineCoords.reverse().slice(1)]; } else { // 无匹配端点直接拼接避免返回原环 mergedLine [...ring.slice(0, -1), ...lineCoords]; } // 闭合环并去重 mergedLine [...new Set(mergedLine.map(pt pt.join(,)))].map(str str.split(,).map(Number)); mergedLine.push(mergedLine[0]); // 验证拼接结果 console.log(拼接前环长度, ring.length); console.log(拼接后环长度, mergedLine.length); return mergedLine; } /** * 空洞归属判断参考分割工具_singleClip * private */ _assignHolesToPolygons(holes, poly1Outer, poly2Outer) { const poly1Holes [], poly2Holes []; const tempPoly1 this.turf.polygon([poly1Outer]); const tempPoly2 this.turf.polygon([poly2Outer]); for (const hole of holes) { const center this.turf.centroid(this.turf.polygon([hole])); const in1 this.turf.booleanPointInPolygon(center, tempPoly1); const in2 this.turf.booleanPointInPolygon(center, tempPoly2); if (in1 !in2) { poly1Holes.push(hole); } else if (in2 !in1) { poly2Holes.push(hole); } else if (in1 in2) { // 同时在两个面内时按距离交点的远近分配 const p1 this.turf.point(poly1Outer[0]); const d1 this.turf.distance(center, p1); const d2 this.turf.distance(center, this.turf.point(poly2Outer[0])); d1 d2 ? poly1Holes.push(hole) : poly2Holes.push(hole); } } return { poly1Holes, poly2Holes }; } /** * 清理多边形修复自相交保留原始坐标 * private */ _cleanPolygon(poly) { let cleaned this.turf.cleanCoords(poly); // 修复自相交不简化坐标 const kinks this.turf.kinks(cleaned); if (kinks.features.length 0) { cleaned this.turf.polygon( cleaned.geometry.coordinates, cleaned.properties ); } return cleaned; } /** * 查找点在坐标数组中的索引参考分割工具_findPointIndex * private */ _findPointIndex(coords, pt) { for (let i 0; i coords.length; i) { if (this.turf.distance(this.turf.point(coords[i]), this.turf.point(pt)) this.EPS) { return i; } } return -1; } /** * 判断点是否在线段上参考分割工具_isPointOnSegment * private */ _isPointOnSegment(pt, a, b) { const cross (pt[0] - a[0]) * (b[1] - a[1]) - (pt[1] - a[1]) * (b[0] - a[0]); if (Math.abs(cross) this.EPS) return false; const minX Math.min(a[0], b[0]), maxX Math.max(a[0], b[0]); const minY Math.min(a[1], b[1]), maxY Math.max(a[1], b[1]); return pt[0] minX - this.EPS pt[0] maxX this.EPS pt[1] minY - this.EPS pt[1] maxY this.EPS; } /** * 移除小面积碎块保留空洞兼容原有方法 * public */ removeSmallFragments(polygonGeoJSON, minArea) { const multiPolygon this.turf.geomType(polygonGeoJSON) Polygon ? this.turf.multiPolygon([polygonGeoJSON.geometry.coordinates]) : polygonGeoJSON; const validPolygons []; multiPolygon.geometry.coordinates.forEach(coords { const poly this.turf.polygon(coords); const area this.turf.area(poly); if (area minArea) { validPolygons.push(coords); } }); const result this.turf.multiPolygon(validPolygons); result.properties { ...polygonGeoJSON.properties, cleaned: true, cleanTime: Date.now(), innerRingsPreserved: true }; return validPolygons.length ? result : polygonGeoJSON; } } // 导出风格对齐分割工具 const polygonTrimmer new PolygonTrimmer(); /** * 对外暴露的整形方法和splitPolygon写法一致 * param {Object} targetPolygon - 目标多边形GeoJSON * param {Object} trimLine - 整形线GeoJSON * returns {Object|null} 整形结果面积大的那个面 */ export function trimPolygon(targetPolygon, trimLine) { try { const trimResult polygonTrimmer.trimPolygon(targetPolygon, trimLine); // 统一返回数组格式和splitPolygon的返回值对齐 return trimResult ? [trimResult] : null; } catch (error) { console.error(多边形整形出错, error); return null; } } /** * 对外暴露的移除小碎块方法 * param {Object} targetPolygon - 目标多边形GeoJSON * param {number} minArea - 最小保留面积 * returns {Object|null} 清理结果 */ export function removeSmallFragments(targetPolygon, minArea) { try { return polygonTrimmer.removeSmallFragments(targetPolygon, minArea); } catch (error) { console.error(移除小碎块出错, error); return null; } } /** * 创建整形线和createSplitLine写法一致 * param {Array} start - 起点 [x,y] * param {Array} end - 终点 [x,y] * returns {Object|null} 整形线GeoJSON */ export function createTrimLine(start, end) { if (!start || !end || start.length ! 2 || end.length ! 2) return null; return turf.lineString([start, end], { name: trim-line }); }四、使用示例import { trimPolygon, createTrimLine, removeSmallFragments } from ./PolygonTrimmer // 1. 准备原始多边形标准GeoJSON const polygon { type: Feature, properties: {}, geometry: { type: Polygon, coordinates: [[ [116.1, 39.1], [116.2, 39.1], [116.2, 39.2], [116.1, 39.2], [116.1, 39.1] ]] } } // 2. 创建修整线 const trimLine createTrimLine([116.12, 39.12], [116.18, 39.18]) // 3. 执行修整 const result trimPolygon(polygon, trimLine) // 4. 输出结果可直接用于Mapbox渲染 console.log(修整完成, result)五、效果图