解决修整时出现自相交问题,出现畸形图斑,导致修整出现自相交问题
结合修整Mapbox GL JS 自研面要素整形工具开发实录_mapboxgljs开发-CSDN博客,博主这篇文章看,其他不变,主要优化polygonTrimmer这个文件内容
javascript
import * as turf from '@turf/turf';
class PolygonTrimmer {
constructor() {
this.turf = turf;
this.EPS = 1e-6; // 经纬度容差,约0.1米,对齐分割工具
this.tolerance = 1e-10; // 浮点判断精度
}
/**
* 核心方法:修整多边形(模拟ArcGIS修整工具)
* 修整线两端在面外、与面边界产生2交点,分割后保留面积更大的图斑
* @param {Object} polygonGeoJSON - 原始多边形 GeoJSON
* @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);
// 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)));
// 4. 手动切割外环,交点可落在顶点
const [clippedRing1, clippedRing2] = this._manualSplitRing(originalOuterRing, p1, p2);
// 5. 截取修整线上p1-p2之间线段
const clippedTrimLineCoords = this._manualSplitLine(lineFeature.geometry.coordinates, p1, p2);
// 6. 拼接两个候选外环(自动处理线段方向,防止回折交叉)
const poly1Outer = this._connectRingAndTrimLine(clippedRing1, clippedTrimLineCoords);
const poly2Outer = this._connectRingAndTrimLine(clippedRing2, clippedTrimLineCoords);
// 自相交检测,剔除畸形候选
const isSelfA = this._hasSelfIntersection(poly1Outer);
const isSelfB = this._hasSelfIntersection(poly2Outer);
console.debug(`候选A自相交:${isSelfA}, 候选B自相交:${isSelfB}`);
// 7. 构建候选面,附带原始空洞
const candidateA = this.turf.polygon([poly1Outer, ...originalHoles]);
const candidateB = this.turf.polygon([poly2Outer, ...originalHoles]);
// 8. 清理几何(去重复点、调整环绕方向)
const cleanedA = this._cleanPolygon(candidateA);
const cleanedB = this._cleanPolygon(candidateB);
// 9. 面积过滤
const areaA = this.turf.area(cleanedA);
const areaB = this.turf.area(cleanedB);
const minValidArea = 0.0001;
// 收集有效候选:面积达标 + 不自相交
const validCandidates = [];
if (!isSelfA && areaA >= minValidArea) validCandidates.push({ feature: cleanedA, area: areaA });
if (!isSelfB && areaB >= minValidArea) validCandidates.push({ feature: cleanedB, area: areaB });
if (validCandidates.length === 0) {
console.error('两个候选面均自相交或面积过小,修整失败');
return null;
}
// 按面积降序,保留最大的
validCandidates.sort((a, b) => b.area - a.area);
const finalResult = validCandidates[0].feature;
finalResult.properties = {
...polygonFeature.properties,
trim: true,
trimTime: Date.now()
};
return finalResult;
} catch (error) {
console.error('修整多边形失败:', error);
return null;
}
}
/**
* 归一化为单Polygon,MultiPolygon直接抛异常
* @private
*/
_normalizeToPolygon(geoJson) {
const ft = this.turf.feature(geoJson.geometry || geoJson);
const geom = ft.geometry;
if (geom.type === 'MultiPolygon') {
throw new Error('不支持MultiPolygon多部件面,请先拆分图斑');
}
return ft;
}
/**
* 获取修整线与边界的两个有效交点,支持交点落在顶点
* @private
*/
_findValidIntersectionPair(boundaryLine, trimLine) {
const intersections = this.turf.lineIntersect(boundaryLine, trimLine).features;
if (intersections.length < 2) return null;
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)) {
isOnSegment = true;
break;
}
}
if (isOnSegment) validPoints.push(pt);
}
if (validPoints.length < 2) return null;
// 交点去重
const uniquePoints = [];
for (const pt of validPoints) {
const duplicated = uniquePoints.some(u =>
this.turf.distance(this.turf.point(u), this.turf.point(pt)) < this.EPS
);
if (!duplicated) uniquePoints.push(pt);
}
if (uniquePoints.length < 2) return null;
// 按修整线上位置排序
uniquePoints.sort((a, b) => {
const locA = this.turf.nearestPointOnLine(trimLine, this.turf.point(a)).properties.location;
const locB = this.turf.nearestPointOnLine(trimLine, this.turf.point(b)).properties.location;
return locA - locB;
});
return uniquePoints.slice(0, 2);
}
/**
* 在原始外环插入交点,切割成两段环片段;顶点不重复插入
* @private
*/
_manualSplitRing(ringCoords, p1, p2) {
const ring = [...ringCoords.slice(0, -1)];
const _projectPointToSegment = (pt) => {
for (let i = 0; i < ring.length - 1; i++) {
const a = ring[i];
const b = ring[i + 1];
if (this._isPointOnSegment(pt, a, b)) return pt;
}
return null;
};
const insertPoint = (rawPt) => {
const pt = _projectPointToSegment(rawPt);
if (!pt) throw new Error(`交点不在外环线段上 ${rawPt}`);
const existIdx = this._findPointIndex(ring, pt);
if (existIdx !== -1) return;
for (let i = 0; i < ring.length - 1; i++) {
const a = ring[i];
const b = ring[i + 1];
if (this._isPointOnSegment(pt, a, b)) {
ring.splice(i + 1, 0, pt);
return;
}
}
throw new Error(`插入交点失败 ${pt}`);
};
insertPoint(p1);
insertPoint(p2);
const i1 = this._findPointIndex(ring, p1);
const i2 = this._findPointIndex(ring, p2);
if (i1 === -1 || i2 === -1) throw new Error('切割外环找不到交点索引');
const minIdx = Math.min(i1, i2);
const maxIdx = Math.max(i1, i2);
const part1 = [...ring.slice(minIdx, maxIdx + 1)];
const part2 = [...ring.slice(maxIdx), ...ring.slice(0, minIdx + 1)];
return [part1, part2];
}
/**
* 在坐标数组内按容差查找点索引
* @private
*/
_findPointIndex(coords, targetPt) {
for (let i = 0; i < coords.length; i++) {
const dist = this.turf.distance(this.turf.point(coords[i]), this.turf.point(targetPt));
if (dist < this.EPS) return i;
}
return -1;
}
/**
* 截取修整线 p1-p2 之间线段
* @private
*/
_manualSplitLine(lineCoords, p1, p2) {
const lineFt = this.turf.lineString(lineCoords);
const loc1 = this.turf.nearestPointOnLine(lineFt, this.turf.point(p1)).properties.location;
const loc2 = this.turf.nearestPointOnLine(lineFt, this.turf.point(p2)).properties.location;
const start = Math.min(loc1, loc2);
const end = Math.max(loc1, loc2);
const sliced = this.turf.lineSliceAlong(lineFt, start, end);
return sliced.geometry.coordinates;
}
/**
* 【核心修复】环片段 + 修整线段自动匹配方向,闭合外环,防止反向折叠三角
* @private
*/
_connectRingAndTrimLine(ringSeg, trimSeg) {
const ringEnd = ringSeg[ringSeg.length - 1];
const trimStart = trimSeg[0];
const trimEnd = trimSeg[trimSeg.length - 1];
const endMatchTrimStart = this.turf.distance(this.turf.point(ringEnd), this.turf.point(trimStart)) < this.EPS;
const endMatchTrimEnd = this.turf.distance(this.turf.point(ringEnd), this.turf.point(trimEnd)) < this.EPS;
let finalTrimSeg;
if (endMatchTrimStart) {
finalTrimSeg = trimSeg;
} else if (endMatchTrimEnd) {
finalTrimSeg = [...trimSeg].reverse();
} else {
throw new Error('环片段终点无法匹配修整线段端点,无法闭合环');
}
return [...ringSeg, ...finalTrimSeg.slice(1), ringSeg[0]];
}
/**
* 判断点是否在线段上(带容差)
* @private
*/
_isPointOnSegment(pt, a, b) {
const distPtA = this.turf.distance(this.turf.point(pt), this.turf.point(a));
const distPtB = this.turf.distance(this.turf.point(pt), this.turf.point(b));
const distAB = this.turf.distance(this.turf.point(a), this.turf.point(b));
return Math.abs((distPtA + distPtB) - distAB) < this.EPS;
}
/**
* 几何清理:去除重复点 + 统一环绕方向
* @private
*/
_cleanPolygon(poly) {
const cleaned = this.turf.cleanCoords(poly, { mutate: false });
return this.turf.rewind(cleaned);
}
/**
* 检测环是否存在真自相交(排除顶点/端点触碰)
* @private
*/
_hasSelfIntersection(ringCoords) {
const line = this.turf.lineString(ringCoords);
const intersectFeatures = this.turf.lineIntersect(line, line).features;
// 过滤掉顶点本身的交点,只保留跨边真实相交
const realCross = intersectFeatures.filter(f => {
const crossPt = f.geometry.coordinates;
return !ringCoords.some(p =>
this.turf.distance(this.turf.point(p), this.turf.point(crossPt)) < this.EPS
);
});
return realCross.length > 0;
}
/**
* 移除小碎面(保留空洞)
* @public
*/
removeSmallFragments(polygonGeoJSON, minArea) {
const multiPoly = this.turf.geomType(polygonGeoJSON) === 'MultiPolygon'
? polygonGeoJSON
: this.turf.multiPolygon([polygonGeoJSON.geometry.coordinates]);
const keepParts = multiPoly.geometry.coordinates.filter(polyCoords => {
const ft = this.turf.polygon(polyCoords);
return this.turf.area(ft) >= minArea;
});
if (keepParts.length === 0) return null;
const result = this.turf.multiPolygon(keepParts);
result.properties = { ...polygonGeoJSON.properties, clean: true };
return result;
}
}
const polygonTrimmer = new PolygonTrimmer();
/**
* 对外接口,返回数组,和分割工具接口对齐
* @param {Object} targetPolygon
* @param {Object} trimLine
* @returns {Array|null}
*/
export function trimPolygon(targetPolygon, trimLine) {
try {
const res = polygonTrimmer.trimPolygon(targetPolygon, trimLine);
return res ? [res] : null;
} catch (err) {
console.error('trimPolygon api error', err);
return null;
}
}
export function removeSmallFragments(targetPolygon, minArea) {
return polygonTrimmer.removeSmallFragments(targetPolygon, minArea);
}