不规则多边形:寻找1个合适的标记点

767 阅读6分钟

在地图场景开发时多数会遇到图上要显示区域的名称、用图标标记区域、或是在区域上弹框显示信息这种功能。这都需要我们在多边形区域上找到一个合适的点位好来显示它们。

area1.png

对于凸多边形,它们的中心、质心总是在多边形上,且计算简单,可以使用这两个点,但对于凹多边形就不一定了。所以我们需要一个计算方法,对“凸多边形、凹多边形”都能找出一个在多边形内的点,用于标记、弹框、显示名称等。

一、多边形最大内切圆计算方法

我们比较希望的结果是找到区域局部内一块范围较大的地方,然后求其中心点。

这和多边形求最大内切圆的问题很相似。因此可尝试使用“多边形最大内切圆相关算法”得到最大内切圆的圆心,以下是收集到的几种方法。

1、网格法

步骤

  • 将多边形所在的区域划分为网格。
  • 计算每个网格点到多边形各边的最短距离。
  • 找出距离最大的网格点作为最大内切圆的圆心。

特点:通过网格化简化了计算过程,但网格的划分精细度会影响结果的精度。

2、枚举法

步骤

  • 枚举多边形的三条边,判断这三条边是否能构成一个内切圆(与其它边不相割)。
  • 如果能,则计算其半径大小。
  • 找出半径最大的内切圆。

特点:对于某些特殊情况(如矩形)可能不适用,且计算量较大。

3、数学优化法

步骤

  • 建立数学优化模型,以圆心和半径为变量,以圆心到多边形各边的距离最大化为目标函数。
  • 使用数学优化算法求解该优化问题。

特点:通常需要一定的数学基础和编程能力,但可以得到精确的结果。

4、基于voronoi图生成

步骤

  • 生成多边形的voronoi图:大致是对多边形顶点使用Delaunay三角剖分算法进行三角剖分,计算各三角形外接圆圆心,然后遍历按条件过滤连接满足条件的点。构建结果会生成多边形的一条中线。
  • 查找最大内切圆:根据生成的voronoi图,对其中线上的各个交点作为圆心计算与各边最近距离作为半径,最后选出半径最大的那个圆心。

特点:精确,但复杂。

5、方法比对

方法3,方法4比较精确,但计算都很复杂,建立数学模型估计会涉及“带条件的极值问题”,而生成voronoi图还得再加一个Delaunay算法!

可能在一些要求精确的场景得用方法3、4,但这里我们不需要多精确。方法1步骤最为简单,所以选用它来计算。以下是一个经过调整后的网格法。

二、调整后的网格法

步骤:

  1. 用几条水平直线(y=k)与多边形各边计算交点。
  2. 步骤1结果必定会得到2n个交点(n≥0),将相邻两个交点为一条线段,如p0,p1,p2,p3中p0,p1为一段,p2,p3为一段。这样筛选出的线段必然是在多边形内的。
  3. 使用垂直的几条直线(x=k)与上面计算得的水平线段再求交点。称这些交点为网格点
  4. 每个网格点使用上一篇提到的SDF方法,计算其与各边最近距离,选取最小距离作为该点的内切圆半径。
  5. 计算出所有网格点的内切圆半径,选择半径最大的那个网格点作为最终结果。

绘图2.png

上面步骤2得到的线段都在多边形内,步骤3计算得到的网格点也必定在多边形内,不用再进行“交点是否在多边形内的判断”,这样整个过程中都只是一些简单的“直线求交、距离计算、判断”逻辑。

代码如下:


/**获取多边形最大内切圆 坐标点
 * @param {[{x,y}]} points 多边形顶点
 * @param {number} grid 比率(0~1)用于计算网格大小 
 */
function getPolygonMaxInnerRadius(points, grid = 0.15) {
  let xLines = [];
  // x,y方向分别从底到高排序
  let pointsSortX = [...points].sort((a, b) => a.x - b.x);
  let pointsSortY = [...points].sort((a, b) => a.y - b.y);
  const minLen = (pointsSortX.slice(-1)[0].x - pointsSortX[0].x) / 5;
  // 网格线的间距
  let stepY = (pointsSortY.slice(-1)[0].y - pointsSortY[0].y) * grid;
  let stepX = (pointsSortX.slice(-1)[0].x - pointsSortX[0].x) * grid;

  let _yls = null;
  const circlePoints = [...points, points[0]];
  // 从多边形边界开始,使用几条水平直线与多边形各边计算交点
  for (let y = pointsSortY[0].y + stepY; y < pointsSortY.slice(-1)[0].y; y += stepY) {
    // 得到直线 y=k 相交到的点。
    _yls = getXLines(y, circlePoints, minLen);
    if (_yls.length === 0) continue;
    xLines = xLines.concat(_yls);
  }

  // 水平线段长度排序
  let linesSortLen = xLines.sort((a, b) => a.length - b.length);
  
  let cr = 0,center = { r: Number.NEGATIVE_INFINITY, x: 0, y: 0 };
  // 用垂直直线与水平线段计算交点,并计算交点的最大内切圆
  for (let x = pointsSortX[0].x; x < pointsSortX.slice(-1)[0].x; x += stepX) {
    linesSortLen.forEach((line) => {
      if (x < line.p1.x || x > line.p2.x) return;
      cr = calcMinRadius({ x, y: line.p1.y }, circlePoints);

      if (cr > center.r) {
        center.r = cr;
        center.x = x;
        center.y = line.p1.y;
      }
    });
  }
  return center;
}
/**计算与cy相交的几条线段,points首与尾相同**/
function getXLines(cy, points, minLen) {
  // 相交点的x坐标
  let cx = 0;
  let crossPoints = [];

  for (let i = 0; i < points.length - 1; i++) {
    // 与直线cy相交的 点x
    cx = getCrossXByY(cy, points[i], points[i + 1]);
    if (cx === false) continue;
    crossPoints.push({ x: cx, y: cy });
  }
  // 按x值排序
  const pointsSortx = crossPoints.sort((p1, p2) => p1.x - p2.x);
  const lineArr = [];
  // 有多段相交线时,0,1为一段, 2,3为一段, 4,5为一段,...
  for (let i = 0; i <= pointsSortx.length - 2; i += 2) {
    // 过滤掉小于固定值的线段
    if (pointsSortx[i + 1].x - pointsSortx[i].x < minLen) continue;
    else {
      lineArr.push({
        p1: pointsSortx[i],
        p2: pointsSortx[i + 1],
        length: pointsSortx[i + 1].x - pointsSortx[i].x,
      });
    }
  }
  return lineArr;
}

function calcMinRadius(p, points) {
  let r = Number.POSITIVE_INFINITY,
    cr = 0;
  for (let i = 0; i < points.length - 1; i++) {
    cr = sdSegment(p, points[i], points[i + 1]);
    if (cr < r) r = cr;
  }

  return r;
}


function dot(v1, v2) {
  return v1.x * v2.x + v1.y * v2.y;
}
function clamp(val, min, max) {
  return Math.min(Math.max(val, min), max);
}
function length(v) {
  return Math.sqrt(dot(v, v));
}
/**线段的 2D SDF
 * @param {{x:Number,y:Number}} p 判断的点
 * @param {{x:Number,y:Number}} a 线段的一端
 * @param {{x:Number,y:Number}} b 线段的一端
 */
function sdSegment(p, a, b) {
  var pa = { x: p.x - a.x, y: p.y - a.y },
    ba = { x: b.x - a.x, y: b.y - a.y };
  var h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  ba.x = ba.x * h;
  ba.y = ba.y * h;

  return length({ x: pa.x - ba.x, y: pa.y - ba.y });
}

// 使用
const areaCenter = getPolygonMaxInnerRadius([{x:35,y:32},{x:125,y:65},{x:81,y:97}]);

三、效果:

这几个区域中间的点是grid=0.1时计算所得到的,凸多边形情况多数不会是比较理想的结果,但凹多边形情况表现不错。

area2.png