在地图场景开发时多数会遇到图上要显示区域的名称、用图标标记区域、或是在区域上弹框显示信息这种功能。这都需要我们在多边形区域上找到一个合适的点位好来显示它们。
对于凸多边形,它们的中心、质心总是在多边形上,且计算简单,可以使用这两个点,但对于凹多边形就不一定了。所以我们需要一个计算方法,对“凸多边形、凹多边形”都能找出一个在多边形内的点,用于标记、弹框、显示名称等。
一、多边形最大内切圆计算方法
我们比较希望的结果是找到区域局部内一块范围较大的地方,然后求其中心点。
这和多边形求最大内切圆的问题很相似。因此可尝试使用“多边形最大内切圆相关算法”得到最大内切圆的圆心,以下是收集到的几种方法。
1、网格法
步骤:
- 将多边形所在的区域划分为网格。
- 计算每个网格点到多边形各边的最短距离。
- 找出距离最大的网格点作为最大内切圆的圆心。
特点:通过网格化简化了计算过程,但网格的划分精细度会影响结果的精度。
2、枚举法
步骤:
- 枚举多边形的三条边,判断这三条边是否能构成一个内切圆(与其它边不相割)。
- 如果能,则计算其半径大小。
- 找出半径最大的内切圆。
特点:对于某些特殊情况(如矩形)可能不适用,且计算量较大。
3、数学优化法
步骤:
- 建立数学优化模型,以圆心和半径为变量,以圆心到多边形各边的距离最大化为目标函数。
- 使用数学优化算法求解该优化问题。
特点:通常需要一定的数学基础和编程能力,但可以得到精确的结果。
4、基于voronoi图生成
步骤:
- 生成多边形的voronoi图:大致是对多边形顶点使用Delaunay三角剖分算法进行三角剖分,计算各三角形外接圆圆心,然后遍历按条件过滤连接满足条件的点。构建结果会生成多边形的一条中线。
- 查找最大内切圆:根据生成的voronoi图,对其中线上的各个交点作为圆心计算与各边最近距离作为半径,最后选出半径最大的那个圆心。
特点:精确,但复杂。
5、方法比对
方法3,方法4比较精确,但计算都很复杂,建立数学模型估计会涉及“带条件的极值问题”,而生成voronoi图还得再加一个Delaunay算法!
可能在一些要求精确的场景得用方法3、4,但这里我们不需要多精确。方法1步骤最为简单,所以选用它来计算。以下是一个经过调整后的网格法。
二、调整后的网格法
步骤:
- 用几条水平直线(
y=k)与多边形各边计算交点。 - 步骤1结果必定会得到
2n个交点(n≥0),将相邻两个交点为一条线段,如p0,p1,p2,p3中p0,p1为一段,p2,p3为一段。这样筛选出的线段必然是在多边形内的。 - 使用垂直的几条直线(
x=k)与上面计算得的水平线段再求交点。称这些交点为网格点。 - 每个网格点使用上一篇提到的SDF方法,计算其与各边最近距离,选取最小距离作为该点的内切圆半径。
- 计算出所有网格点的内切圆半径,选择半径最大的那个网格点作为最终结果。
上面步骤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时计算所得到的,凸多边形情况多数不会是比较理想的结果,但凹多边形情况表现不错。