优化修整算法

解决修整时出现自相交问题,出现畸形图斑,导致修整出现自相交问题

结合修整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);
}
相关推荐
达子6661 小时前
RUST 图解 第 3 章:通用编程概念(Common Programming Concepts)
开发语言·算法·rust
夏幻灵2 小时前
每日一题:LeetCode 165:比较版本号
算法·leetcode·职场和发展
道尔柯南2 小时前
C++ 面向对象编程入门:类和对象(上)
开发语言·c++·算法
楠楠子呀2 小时前
英语、西语、阿语客户各开一个口:多语言获客的分流方案
开发语言·javascript·ecmascript·跨境电商·whatsapp·营销工具
Navigator_Z2 小时前
LeetCode //C - 1288. Remove Covered Intervals
c语言·算法·leetcode
weifont2 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
码爸2 小时前
决策树介绍与示例
python·算法·机器学习
米柆2 小时前
HTML5 audio /video 完整事件监听模板
前端·html·html5
半个落月2 小时前
LangGraph.js 实战:从状态图到分支、循环、检查点与人工中断
javascript·人工智能·langchain