多边形的内缩算法(二)—单边内缩

Posted by Williamic on 2020-08-22

1.前言

   在多边形的内缩算法(一)中,介绍了全边内缩和外扩的思路和方法,但实际项目中有特定边内缩的需求(比如特定边界线外侧存在障碍物,为了安全起见需要内缩),故而在全边内缩的思路上进行改进和优化,衍生出单边内缩算法。先上最终的效果图,下图的每条边界内缩距离不相等

2.概述

1)与全边内缩相比的差异

   对于上一篇文章中提到的全边内缩(外扩)来说,因为每条边的内缩(外扩)距离都是相同的,故而新的边界点相对于当前点的位置来说都是在特定方向上的——当前点的两邻边单位向量(v1,v2)构成的法线(T)上。如下图所示。
法线
   而对于单边内缩来说,因为邻边的内缩距离和当前边并不一致,且会遇到某条边的内缩边与当前边的邻边的无交点的情况,所以无法通过全边内缩的方式来计算,如下图所示,AB边的内缩边与左邻边AF边有交点p1,但与右邻边BC边无交点,与CD边有交点p2。与当前边的邻边的无交点

2)需要注意的地方

   对于单边内缩来说,除了上面提到过的内缩边与左右邻边无交点的情况(也就是需要对多边形当前边以外的边依次做交点判断),最重要的就是需要确定与当前边平行的左右两侧边哪条为内缩边(下面会提到如何判断)。倘若以全边内缩的方式同时计算左右两侧的平行边,再用平行边与多边形其他边产生交点的位置来筛选交点,在内凹型地块中会无法进行区分,如下图所示4个点都在多边形内(边上),无法判断哪两个为内缩点。内凹地块交点

3.单边内缩的实现方式

1)判断边界点的顺逆时针

   在添加地块边界点的过程中,会按照添加点的顺序来构成多边形,所以对于顺时针的地块边界来说,内缩边都在以多边形的边界点为向量(向量方向从低序号的点到高序号的点)的右侧,对于逆时针的多边形来说,内缩边都在向量的左侧。因此结合2.2提到的,只需要判断多边形的顺逆时针,就可以在后续计算知道哪条平行边为内缩边。
   代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 判断多边形是顺时针还是逆时针
* @param oldLatLongs
* @return
*/
private static boolean isClockwise(List<LatLong> oldLatLongs){
LatLong origin = oldLatLongs.get(0);
//转换为平面坐标点计算
List<Point2D> point2DS = loc2Point2D(origin, oldLatLongs);

double pointMaxY = Double.MAX_VALUE;
int index = 0;
//取多边形Y轴最高点的index序号
for (int i = 0; i < oldLatLongs.size(); i++) {
if (pointMaxY == Double.MAX_VALUE) {
pointMaxY = point2DS.get(i).y;
index = i;
} else if (pointMaxY < point2DS.get(i).y) {
pointMaxY = point2DS.get(i).y;
index = i;
}
}

//最高点的左侧点
Point2D leftPoint = point2DS.get((index + oldLatLongs.size() - 1) % oldLatLongs.size());
//最高点
Point2D midPoint = point2DS.get(index);
//最高点的右侧点
Point2D rightPoint = point2DS.get((index + 1) % oldLatLongs.size());

Vector2D vRight = new Vector2D(rightPoint.x - midPoint.x, rightPoint.y - midPoint.y);
Vector2D vLeft = new Vector2D(midPoint.x - leftPoint.x, midPoint.y - leftPoint.y);

//向量积 >0 说明vLeft在vRight的左侧,所以此时为顺时针,反之则为逆时针
return vRight.crossProduct(vLeft) > 0;//顺时针
}

2)判断内缩边

   首先将传入的边界点转换为以第一个边界点为原点的平面坐标点,再将点转换为线的集合。取当前的线做向量v,再根据向量v和内缩的距离作平行于它的平行线。接下来判断平行线在的位置是否和多边形的顺逆时针相同。
   取当前边的中点作垂直于x轴的垂线,垂线与平行线有交点T,此时以当前边和交点T作一条向量Vt,向量方向为边的起点到交点T,此时根据向量Vt和向量v的向量积,判断平行边是否为内缩边(若多边形为顺时针,且向量Vt在向量v的右侧,则当前的平行边为内缩边,否则内缩边为另一个方向的平行边),如下图所示,此时L1为内缩边。内缩边    代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
LatLong origin = oldLatLongs.get(0);
List<Point2D> point2DS = loc2Point2D(origin, oldLatLongs);
List<Line2D> line2DS = point2Line(point2DS);

//当前边
Line2D midLine = line2DS.get(index);

Vector2D vector = new Vector2D(midLine.endPoint.x - midLine.startPoint.x,
midLine.endPoint.y - midLine.startPoint.y);
//做平行边
Line2D ParallelLine;

if (vector.getRadian() == Math.PI / 2 || vector.getRadian() == -Math.PI / 2) {
double space = dis * 100.0f;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.a * space);
} else {
double cos = Math.abs(Math.cos(vector.getRadian()));
double space = dis * 100.0f / cos;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.b * space);
}

//平行边与当前边的中点作垂直于X轴的直线的交点
Point2D pointT= ParallelLine.getIntersectPoint2(new Line2D(1,0,-(midLine.endPoint.x+midLine.startPoint.x)/2));
Vector2D vectorT = new Vector2D(pointT.x-midLine.startPoint.x,pointT.y-midLine.startPoint.y);
//判断方向是否与顺逆时针相匹配
double T = vector.crossProduct(vectorT);//T != 0
if ((clockwise && T > 0) || (!clockwise && T < 0)) {//顺逆不相同
if (vector.getRadian() == Math.PI / 2 || vector.getRadian() == -Math.PI / 2) {
double space = -dis * 100.0f;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.a * space);
} else {
double cos = Math.abs(Math.cos(vector.getRadian()));
double space = -dis * 100.0f / cos;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.b * space);
}
}

3)计算内缩边与其他边的交点

   确定了内缩边之后,接下来对多边形的其他边进行遍历,计算内缩边与多边形的其他边的交点位置,再判断交点是否在原多边形内(判断点在多边形内外)。如果交点在多边形外部,则说明内缩边与多边形的这条边无交点,需要继续对其他边遍历。如下图所示,点O就与内缩边的交点在多边形外部,此时则认为内缩边与BC边无交点,于是对CD边进行计算。内凹地块交点
   代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Pair<Point2D, Integer> pointLeftPair = null;
Pair<Point2D, Integer> pointRightPair = null;

for (int i = 1; i < oldLatLongs.size() - 1; i++) {
//左侧邻边序号,递减
int leftIndex = (index + oldLatLongs.size() - i) % oldLatLongs.size();
//右邻边序号,递增
int rightIndex = (index + i) % oldLatLongs.size();

if (pointLeftPair == null){
Point2D point1 = ParallelLine.getIntersectPoint2(line2DS.get(leftIndex));
//判断point1是否在多边形内部
if (point1 != null && isInPolygon(point2D2Latlong(origin, point1), oldLatLongs)){
pointLeftPair = new Pair<>(point1, leftIndex);
}
}
if (pointRightPair == null){
Point2D point2 = ParallelLine.getIntersectPoint2(line2DS.get(rightIndex));
//判断point2是否在多边形内部
if (point2 != null && isInPolygon(point2D2Latlong(origin, point2), oldLatLongs)){
pointRightPair = new Pair<>(point2, rightIndex);
}
}
//遍历所有边后还是无交点
if (pointLeftPair != null && pointRightPair != null){
break;
}
//左侧边与右侧边重合,结束遍历
if (Math.abs(leftIndex - rightIndex) <= 1) {
break;
}
}

4)替换原先坐标点

   当计算出内缩边与多边形其他边的交点之后,最后一步就是将计算出来的点对原先到坐标点进行替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (pointLeftPair == null || pointRightPair == null){
throw new OutOfRangeException("内缩超出范围");
}else {
int leftPairIndex = pointLeftPair.second;
int rightPairIndex = pointRightPair.second;

int max = Math.max(leftPairIndex,rightPairIndex);
int min = Math.min(leftPairIndex,rightPairIndex);
//内缩边与其他边的两个交点正好在当前边的左右邻边上
if ((max - 1) % oldLatLongs.size() == (min + 1) % oldLatLongs.size()) {
point2DS.set((pointLeftPair.second + 1)%oldLatLongs.size(), pointLeftPair.first);
point2DS.set(pointRightPair.second, pointRightPair.first);
}else {
point2DS.set((pointLeftPair.second + 1)%oldLatLongs.size(), pointLeftPair.first);
point2DS.set(pointRightPair.second, pointRightPair.first);
if (rightPairIndex < leftPairIndex) {
rightPairIndex = rightPairIndex + oldLatLongs.size();
}
for (int i = rightPairIndex-1; i > leftPairIndex + 1; i--) {
point2DS.remove((i - 1) % oldLatLongs.size()+1);
}
}
//将新的边界点转换成坐标
return point2D2Latlong(origin, pointActual);
}

4.全部代码

1)单边内缩版本

   上面讲解的代码都是只针对一条进行内缩的场景,全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
private static List<LatLong> narrowSingle(List<LatLong> oldLatLongs, int index, float dis, boolean clockwise) throws OutOfRangeException {
LatLong origin = oldLatLongs.get(0);
List<Point2D> point2DS = loc2Point2D(origin, oldLatLongs);
List<Line2D> line2DS = point2Line(point2DS);

Line2D midLine = line2DS.get(index);

Vector2D vector = new Vector2D(midLine.endPoint.x - midLine.startPoint.x,
midLine.endPoint.y - midLine.startPoint.y);
//平行线
Line2D ParallelLine;

if (vector.getRadian() == Math.PI / 2 || vector.getRadian() == -Math.PI / 2) {
double space = dis * 100.0f;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.a * space);
} else {
double cos = Math.abs(Math.cos(vector.getRadian()));
double space = dis * 100.0f / cos;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.b * space);
}

Point2D pointT= ParallelLine.getIntersectPoint2(new Line2D(1,0,-(midLine.endPoint.x+midLine.startPoint.x)/2));
Vector2D vectorT = new Vector2D(pointT.x-midLine.startPoint.x,pointT.y-midLine.startPoint.y);
double T = vector.crossProduct(vectorT);//T != 0
if ((clockwise && T > 0) || (!clockwise && T < 0)) {//顺逆不相同
if (vector.getRadian() == Math.PI / 2 || vector.getRadian() == -Math.PI / 2) {
double space = -dis * 100.0f;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.a * space);
} else {
double cos = Math.abs(Math.cos(vector.getRadian()));
double space = -dis * 100.0f / cos;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.b * space);
}
}
Pair<Point2D, Integer> pointLeftPair = null;
Pair<Point2D, Integer> pointRightPair = null;

for (int i = 1; i < oldLatLongs.size() - 1; i++) {
//左侧邻边序号,递减
int leftIndex = (index + oldLatLongs.size() - i) % oldLatLongs.size();
//右邻边序号,递增
int rightIndex = (index + i) % oldLatLongs.size();

if (pointLeftPair == null){
Point2D point1 = ParallelLine.getIntersectPoint2(line2DS.get(leftIndex));
//判断point1是否在多边形内部
if (point1 != null && isInPolygon(point2D2Latlong(origin, point1), oldLatLongs)){
pointLeftPair = new Pair<>(point1, leftIndex);
}
}
if (pointRightPair == null){
Point2D point2 = ParallelLine.getIntersectPoint2(line2DS.get(rightIndex));
//判断point2是否在多边形内部
if (point2 != null && isInPolygon(point2D2Latlong(origin, point2), oldLatLongs)){
pointRightPair = new Pair<>(point2, rightIndex);
}
}
//遍历所有边后还是无交点
if (pointLeftPair != null && pointRightPair != null){
break;
}
//左侧边与右侧边重合,结束遍历
if (Math.abs(leftIndex - rightIndex) <= 1) {
break;
}
}

if (pointLeftPair == null || pointRightPair == null){
throw new OutOfRangeException("内缩超出范围");
}else {
int leftPairIndex = pointLeftPair.second;
int rightPairIndex = pointRightPair.second;

int max = Math.max(leftPairIndex,rightPairIndex);
int min = Math.min(leftPairIndex,rightPairIndex);
//内缩边与其他边的两个交点正好在当前边的左右邻边上
if ((max - 1) % oldLatLongs.size() == (min + 1) % oldLatLongs.size()) {
point2DS.set((pointLeftPair.second + 1)%oldLatLongs.size(), pointLeftPair.first);
point2DS.set(pointRightPair.second, pointRightPair.first);
}else {
point2DS.set((pointLeftPair.second + 1)%oldLatLongs.size(), pointLeftPair.first);
point2DS.set(pointRightPair.second, pointRightPair.first);
if (rightPairIndex < leftPairIndex) {
rightPairIndex = rightPairIndex + oldLatLongs.size();
}
for (int i = rightPairIndex-1; i > leftPairIndex + 1; i--) {
point2DS.remove((i - 1) % oldLatLongs.size()+1);
}
}
}
return point2D2Latlong(origin, point2DS);
}

2)多边内缩版本

   多边内缩是单边内缩的集成版,本质上是所有边的不同内缩距离进行递归,但需要注意的是,在单边内缩的时候,内缩边与邻边无交点的情况会导致下一次递归的边的index序号混乱,因此需要计算原始边界点和内缩过程中的边界差值(内缩边与邻边无交点的情况会导致实际边界数量少于原始边界)。
   全部代码如下,引入temp值做差值计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

public static List<LatLong> narrowTest(List<LatLong> oldLatLongs, List<Float> dis){
//计算地块的顺逆时针
boolean wise = isClockwise(oldLatLongs);
//原始边界点
List<LatLong> originalLatLongs = new ArrayList<>(oldLatLongs);
for (int i = 0; i < dis.size(); i++) {
originalLatLongs = narrowSingle(oldLatLongs,originalLatLongs, i, dis.get(i), wise);
}
return originalLatLongs;
}

/**
*
* @param oldLatLongs 内缩边界点
* @param originalLatLongs 原始边界点
* @param index 内缩边序号
* @param dis 内缩距离
* @param clockwise 顺逆时针
* @return
*/
private static List<LatLong> narrowSingle(List<LatLong> oldLatLongs,List<LatLong> originalLatLongs, int index, float dis, boolean clockwise) {
LatLong origin = oldLatLongs.get(0);
List<Point2D> point2DS = loc2Point2D(origin, oldLatLongs);
List<Line2D> line2DS = point2Line(point2DS);

Line2D midLine = line2DS.get(index);

Vector2D vector = new Vector2D(midLine.endPoint.x - midLine.startPoint.x,
midLine.endPoint.y - midLine.startPoint.y);
//平行线
Line2D ParallelLine;

if (vector.getRadian() == Math.PI / 2 || vector.getRadian() == -Math.PI / 2) {
double space = dis * 100.0f;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.a * space);
} else {
double cos = Math.abs(Math.cos(vector.getRadian()));
double space = dis * 100.0f / cos;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.b * space);
}

Point2D pointT= ParallelLine.getIntersectPoint2(new Line2D(1,0,-(midLine.endPoint.x+midLine.startPoint.x)/2));
Vector2D vectorT = new Vector2D(pointT.x-midLine.startPoint.x,pointT.y-midLine.startPoint.y);
double T = vector.crossProduct(vectorT);//T != 0
if ((clockwise && T > 0) || (!clockwise && T < 0)) {//顺逆不相同
if (vector.getRadian() == Math.PI / 2 || vector.getRadian() == -Math.PI / 2) {
double space = -dis * 100.0f;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.a * space);
} else {
double cos = Math.abs(Math.cos(vector.getRadian()));
double space = -dis * 100.0f / cos;//对应的Y轴截距
ParallelLine = new Line2D(midLine.a, midLine.b, midLine.c - midLine.b * space);
}
}
Pair<Point2D, Integer> pointLeftPair = null;
Pair<Point2D, Integer> pointRightPair = null;

List<Point2D> pointActual = loc2Point2D(origin, originalLatLongs);
List<Line2D> lineActual = point2Line(pointActual);

//用于计算原始边界与内缩边界的差值
int temp = oldLatLongs.size() - originalLatLongs.size();

for (int i = 1; i < originalLatLongs.size() - 1; i++) {
//左侧邻边序号,递减
int leftIndex = (index + originalLatLongs.size() - i - temp) % originalLatLongs.size();
//右邻边序号,递增
int rightIndex = (index + i - temp) % originalLatLongs.size();

if (pointLeftPair == null){
Point2D point1 = ParallelLine.getIntersectPoint2(line2DS.get(leftIndex));
//判断point1是否在多边形内部
if (point1 != null && isInPolygon(point2D2Latlong(origin, point1), oldLatLongs)){
pointLeftPair = new Pair<>(point1, leftIndex);
}
}
if (pointRightPair == null){
Point2D point2 = ParallelLine.getIntersectPoint2(line2DS.get(rightIndex));
//判断point2是否在多边形内部
if (point2 != null && isInPolygon(point2D2Latlong(origin, point2), oldLatLongs)){
pointRightPair = new Pair<>(point2, rightIndex);
}
}
//遍历所有边后还是无交点
if (pointLeftPair != null && pointRightPair != null){
break;
}
//左侧边与右侧边重合,结束遍历
if (Math.abs(leftIndex - rightIndex) <= 1) {
break;
}
}

if (pointLeftPair == null || pointRightPair == null) {
//如果无交点则返回上一次的边界点
return originalLatLongs;
} else {
int leftPairIndex = pointLeftPair.second;
int rightPairIndex = pointRightPair.second;

int max = Math.max(leftPairIndex, rightPairIndex);
int min = Math.min(leftPairIndex, rightPairIndex);
//内缩边与其他边的两个交点正好在当前边的左右邻边上
if ((max - 1) % originalLatLongs.size() == (min + 1) % originalLatLongs.size()) {
pointActual.set((pointLeftPair.second + 1) % originalLatLongs.size(), pointLeftPair.first);
pointActual.set(pointRightPair.second, pointRightPair.first);
} else {
pointActual.set((pointLeftPair.second + 1) % originalLatLongs.size(), pointLeftPair.first);
pointActual.set(pointRightPair.second, pointRightPair.first);
if (rightPairIndex < leftPairIndex) {
rightPairIndex = rightPairIndex + originalLatLongs.size();
}
for (int i = rightPairIndex - 1; i > leftPairIndex + 1; i--) {
pointActual.remove((i - 1) % originalLatLongs.size() + 1);
}
}
}
return point2D2Latlong(origin, pointActual);
}

5.总结

   单边内缩的思路步骤:

  • 首先判断多边形的方向(顺逆时针),用于后续判断内缩边的位置
  • 利用向量积判断两个向量的位置关系方法,来判断内缩边是在当前边的左侧还是右侧,同时可以减少计算量
  • 通过计算与其他边的交点并判断交点的位置来得到真正的新边界点
  • 在原先的坐标点集合中换入新的边界点

   注意:在多变内缩的时候需要注意内缩后的边界点的实际边数量(可能会少边),其他的与单边内缩无异。