如果需要详细的地图应用步骤可参考博主的另一篇分割文章 :https://blog.csdn.net/qq_47285211/article/details/159544070?spm=1011.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 结合实现交互式图斑编辑
三、完整实现代码(可直接复制使用)
javascript
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' });
}
四、使用示例
javascript
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)
五、效果图

