C
creation-zy
Unregistered / Unconfirmed
GUEST, unregistred user!
前两天接到一个网友的来信,提出要解决判断一个点是否在单连通多边形区域之内的问题,
我搜索了一下以前的帖子,看见论坛上有很多这方面的问题,答案也有很多。
(大家可以看一看下面的帖子)
http://www.delphibbs.com/delphibbs/dispq.asp?lid=0997458
http://www.delphibbs.com/delphibbs/dispq.asp?lid=0093903
另:关于多边形的面积,大家可以看
http://www.delphibbs.com/delphibbs/dispq.asp?lid=0170597 其中yysun老师的回答
使用API函数PtInRegion无疑是非常简单的方法,但是它仅能用于座标为整数的情况,有
局限性。网友们还提出了很多算法,其中最普遍的是“跳栏”法——即根据从待考察点发出
的一条射线与多边形的边相交的次数进行判断,我研究了一下,似乎还有商榷的余地。
我在此自创了一种新的算法。源代码及说明如下:
原来写的算法在判断交点位于多边形顶点的情况时会出现错误,究其原因,还是没有考虑
周全造成的。我们现在再次进行详细的分析:
首先,判断点是否在多边形的边界线上(包括顶点),如果在,则认为点在多边形内部。
如果点不在多边形的边界上,我们就过待考查点做一条正向水平射线,并对它与多边形的
边界相交的情况进行分析:
(需要注意的是:由于做的是水平线,所以所有水平方向的边界都不用参加计算——在上面
已经将位于边界上的情况过滤掉了,对于不在水平边界上、但是y坐标又和水平边界一致的
情况,在下面的计算中不用考虑)
1.没有交点
说明点在多边形之外
2.有交点
找到距离待考查点最近的那个交点;
2.1.该交点位于直线上
可以直接根据直线的穿越方向进行判断:由于多边形区域始终处于边界前进方向的右侧,
如果边界自下而上穿越水平线,那么说明点在多边形之外;反之则说明在内部。
2.2.该交点位于多边形的顶点上
假设这个顶点和位于它两边的顶点按照多边形顶点的顺时针的排列顺序分别为A,B,C;
如果线段AB和BC都是向上穿越水平线,那么说明点在多边形之外;反之,如果它们都向
下穿越水平线,那么说明点在多边形之内;
如果在上述这两种情况之外,那么就要考察下面这四种情况:(假设待考查点为O)
A C
C A
O - - B - - > O - - B - - >
O - - B - - > O - - B - - >
C
A C A
我们可以发现,可以根据直线AB以及BC的斜率关系判断点B是多边形的局部“凸”点还
是局部“凹”点。如果是凸点,那么说明O点必在多边形之外;反之则说明在多边形之内。
type
TBorderRec=Record //点的结构
X,Youble;
end;
TPolygon=class
private
FData:array of TBorderRec;
FPointCount: Integer;
FMinX:do
uble;
FMinY:do
uble;
FMaxX:do
uble;
FMaxY:do
uble;
function Get(Index: Integer): TBorderRec;
procedure Put(Index: Integer;
const Value: TBorderRec);
public
property MaxXouble read FMaxX;
property MaxYouble read FMaxY;
property MinXouble read FMinX;
property MinYouble read FMinY;
property PointCount:Integer read FPointCount;
property Items[Index:Integer]:TBorderRec read Get write Put;
default;
procedure Clear;
function PointInside(x,youble):Boolean;
procedure LoadFromFile(const FileName:String);
constructor Create;
destructor Destroy;
override;
end;
{ TPolygon }
procedure TPolygon.Clear;
begin
SetLength(FData,0);
FPointCount:=0;
FMinX:=0;
FMinY:=0;
FMaxX:=0;
FMaxY:=0;
end;
constructor TPolygon.Create;
begin
Clear;
end;
destructor TPolygon.Destroy;
begin
Clear;
inherited;
end;
function TPolygon.Get(Index: Integer): TBorderRec;
begin
if (Index>=0) and (Index<FPointCount) then
Result:=FData[Index]
else
raise Exception.Create('数组访问越界!');
end;
//从文件获得数据,每行存放一个座标值,x和y之间用逗号或空格分隔
//点应该呈顺时针方向,且最后一个点应该和第一个点一致,以封闭多边形
procedure TPolygon.LoadFromFile(const FileName: String);
var
i,n:Integer;
x,y,MaxX,MaxY,MinX,MinYouble;
SL,msl:TStringList;
begin
Clear;
SL:=TStringList.Create;
msl:=TStringList.Create;
MaxX:=0;
MinX:=0;
MaxY:=0;
MinY:=0;
try
SL.LoadFromFile(FileName);
n:=SL.Count;
SetLength(FData,n);
for i:=0 to n-1do
begin
msl.CommaText:=SL;
if msl.Count<>2 then
raise Exception.Create('无效的数据格式!');
x:=StrToFloat(msl[0]);
y:=StrToFloat(msl[1]);
if i=0 then
begin
MaxX:=x;
MinX:=x;
MaxY:=y;
MinY:=y;
end
else
begin
if x>MaxX then
MaxX:=x
else
if x<MinX then
MinX:=x;
if y>MaxY then
MaxY:=y
else
if y<MinY then
MinY:=y;
end;
FData.X:=x;
FData.Y:=y;
end;
FPointCount:=n;
FMaxX:=MaxX;
FMaxY:=MaxY;
FMinX:=MinX;
FMinY:=MinY;
finally
SL.Free;
msl.Free;
end;
end;
function TPolygon.PointInside(x, y:do
uble): Boolean;
function PointOnLine(x1,y1,x2,y2ouble):Boolean;
var
xx,yyouble;
begin
if ((x<x1) and (x<x2)) or ((y<y1) and (y<y2))
or ((x>x1) and (x>x2)) or ((y>y1) and (y>y2)) then
begin
Result:=false;
exit;
end;
xx:=x-x1;
yy:=y-y1;
x2:=x2-x1;
y2:=y2-y1;
if x2=0 then
Result:=xx=0 //在垂线上
else
if y2=0 then
Result:=yy=0 //在水平线上
else
Result:=xx*y2=yy*x2;
end;
function Between(f,f1,f2ouble):Boolean;
begin
if f1<f2 then
Result:=(f>=f1) and (f<=f2)
else
Result:=(f<=f1) and (f>=f2);
end;
function IntersectionDistance(x1,y1,x2,y2ouble)ouble;
begin
//获得直线与水平线的交点到待考查点的距离——带方向(右侧为正)
x1:=x1-x;
x2:=x2-x;
if x1=x2 then
begin
//垂直穿越
Result:=x1;
exit;
end;
y1:=y1-y;
y2:=y2-y;
Result:=x2+y2*(x2-x1)/(y1-y2);
end;
var
i,MinPoint,SameMinPoint:Integer;
MinDistance,tmpfouble;
x1,y1,x2,y2ouble;
begin
Result:=false;
if ((x<FMinX) or (x>FMaxX)) and ((y<FMinY) or (y>FMaxY)) then
exit;
//判断是否落在多边形的边上(包含顶点)
for i:=0 to FPointCount-2do
if PointOnLine(FData.X,FData.Y,FData[i+1].X,FData[i+1].Y) then
begin
Result:=true;
exit;
end;
MinDistance:=0;
MinPoint:=-1;
SameMinPoint:=-1;
for i:=0 to FPointCount-2do
if ((FData.X>=x) or (FData[i+1].X>=x)) and
(FData.Y<>FData[i+1].Y) then
//过滤整体位于左侧的线段以及水平线
if Between(y,FData.Y,FData[i+1].Y) then
begin
tmpf:=IntersectionDistance(FData.X,FData.Y,FData[i+1].X,FData[i+1].Y);
if tmpf>0 then
//因为点不在直线上,所以交点的距离不可能等于0
if MinPoint=-1 then
begin
MinPoint:=i;
MinDistance:=tmpf;
end
else
if tmpf<MinDistance then
//找到一个距离更小的
begin
MinPoint:=i;
SameMinPoint:=-1;
MinDistance:=tmpf;
end
else
if tmpf=MinDistance then
//遇到距离同样近的情况,记下前一个的位置
begin
SameMinPoint:=MinPoint;
MinPoint:=i;
end;
end;
if (MinPoint>=0) and (SameMinPoint<0) then
//有交点,且最近交点不是多边形的顶点
Result:=FData[MinPoint].Y>FData[MinPoint+1].Y //根据穿越方向进行判别
else
if SameMinPoint>=0 then
//有交点,但是是多边形的顶点
begin
if SameMinPoint>MinPoint then
begin
i:=SameMinPoint;
SameMinPoint:=MinPoint;
MinPoint:=i;
end;
y1:=FData[SameMinPoint].Y-FData[MinPoint].Y;
y2:=FData[MinPoint].Y-FData[MinPoint+1].Y;
//由于已经过滤了水平重合的线段...
if (y1>0) and (y2>0) then
//两条线都是自上而下穿越水平线
Result:=true
else
if (y1<0) and (y2<0) then
//两条线都是自下而上穿越水平线
Result:=false
else
begin
//两条直线有升有降——位于水平线的同一侧
x1:=FData[SameMinPoint].X-FData[MinPoint].X;
x2:=FData[MinPoint].X-FData[MinPoint+1].X;
Result:=x1*y2>x2*y1;
//根据两条直线的斜率大小进行判断
end;
end;
end;
procedure TPolygon.Put(Index: Integer;
const Value: TBorderRec);
begin
if (Index>=0) and (Index<FPointCount) then
FData[Index]:=Value
else
raise Exception.Create('数组访问越界!');
end;
给大家一组测试用数据:
0,-0.5
0,0
0.5,-1
1,0
1,-1
0,-2
1,-1.5
2,-3
1,-4
1.25,-3.5
1,-2.5
0,-2.5
-0.5,-3.5
-1,-2.5
-1.5,-3.1
-2,-2.5
-1.5,-2.8
-1,-1
0,-0.5
欢迎查错、提出改进意见!(分数可以另外给)
我搜索了一下以前的帖子,看见论坛上有很多这方面的问题,答案也有很多。
(大家可以看一看下面的帖子)
http://www.delphibbs.com/delphibbs/dispq.asp?lid=0997458
http://www.delphibbs.com/delphibbs/dispq.asp?lid=0093903
另:关于多边形的面积,大家可以看
http://www.delphibbs.com/delphibbs/dispq.asp?lid=0170597 其中yysun老师的回答
使用API函数PtInRegion无疑是非常简单的方法,但是它仅能用于座标为整数的情况,有
局限性。网友们还提出了很多算法,其中最普遍的是“跳栏”法——即根据从待考察点发出
的一条射线与多边形的边相交的次数进行判断,我研究了一下,似乎还有商榷的余地。
我在此自创了一种新的算法。源代码及说明如下:
原来写的算法在判断交点位于多边形顶点的情况时会出现错误,究其原因,还是没有考虑
周全造成的。我们现在再次进行详细的分析:
首先,判断点是否在多边形的边界线上(包括顶点),如果在,则认为点在多边形内部。
如果点不在多边形的边界上,我们就过待考查点做一条正向水平射线,并对它与多边形的
边界相交的情况进行分析:
(需要注意的是:由于做的是水平线,所以所有水平方向的边界都不用参加计算——在上面
已经将位于边界上的情况过滤掉了,对于不在水平边界上、但是y坐标又和水平边界一致的
情况,在下面的计算中不用考虑)
1.没有交点
说明点在多边形之外
2.有交点
找到距离待考查点最近的那个交点;
2.1.该交点位于直线上
可以直接根据直线的穿越方向进行判断:由于多边形区域始终处于边界前进方向的右侧,
如果边界自下而上穿越水平线,那么说明点在多边形之外;反之则说明在内部。
2.2.该交点位于多边形的顶点上
假设这个顶点和位于它两边的顶点按照多边形顶点的顺时针的排列顺序分别为A,B,C;
如果线段AB和BC都是向上穿越水平线,那么说明点在多边形之外;反之,如果它们都向
下穿越水平线,那么说明点在多边形之内;
如果在上述这两种情况之外,那么就要考察下面这四种情况:(假设待考查点为O)
A C
C A
O - - B - - > O - - B - - >
O - - B - - > O - - B - - >
C
A C A
我们可以发现,可以根据直线AB以及BC的斜率关系判断点B是多边形的局部“凸”点还
是局部“凹”点。如果是凸点,那么说明O点必在多边形之外;反之则说明在多边形之内。
type
TBorderRec=Record //点的结构
X,Youble;
end;
TPolygon=class
private
FData:array of TBorderRec;
FPointCount: Integer;
FMinX:do
uble;
FMinY:do
uble;
FMaxX:do
uble;
FMaxY:do
uble;
function Get(Index: Integer): TBorderRec;
procedure Put(Index: Integer;
const Value: TBorderRec);
public
property MaxXouble read FMaxX;
property MaxYouble read FMaxY;
property MinXouble read FMinX;
property MinYouble read FMinY;
property PointCount:Integer read FPointCount;
property Items[Index:Integer]:TBorderRec read Get write Put;
default;
procedure Clear;
function PointInside(x,youble):Boolean;
procedure LoadFromFile(const FileName:String);
constructor Create;
destructor Destroy;
override;
end;
{ TPolygon }
procedure TPolygon.Clear;
begin
SetLength(FData,0);
FPointCount:=0;
FMinX:=0;
FMinY:=0;
FMaxX:=0;
FMaxY:=0;
end;
constructor TPolygon.Create;
begin
Clear;
end;
destructor TPolygon.Destroy;
begin
Clear;
inherited;
end;
function TPolygon.Get(Index: Integer): TBorderRec;
begin
if (Index>=0) and (Index<FPointCount) then
Result:=FData[Index]
else
raise Exception.Create('数组访问越界!');
end;
//从文件获得数据,每行存放一个座标值,x和y之间用逗号或空格分隔
//点应该呈顺时针方向,且最后一个点应该和第一个点一致,以封闭多边形
procedure TPolygon.LoadFromFile(const FileName: String);
var
i,n:Integer;
x,y,MaxX,MaxY,MinX,MinYouble;
SL,msl:TStringList;
begin
Clear;
SL:=TStringList.Create;
msl:=TStringList.Create;
MaxX:=0;
MinX:=0;
MaxY:=0;
MinY:=0;
try
SL.LoadFromFile(FileName);
n:=SL.Count;
SetLength(FData,n);
for i:=0 to n-1do
begin
msl.CommaText:=SL;
if msl.Count<>2 then
raise Exception.Create('无效的数据格式!');
x:=StrToFloat(msl[0]);
y:=StrToFloat(msl[1]);
if i=0 then
begin
MaxX:=x;
MinX:=x;
MaxY:=y;
MinY:=y;
end
else
begin
if x>MaxX then
MaxX:=x
else
if x<MinX then
MinX:=x;
if y>MaxY then
MaxY:=y
else
if y<MinY then
MinY:=y;
end;
FData.X:=x;
FData.Y:=y;
end;
FPointCount:=n;
FMaxX:=MaxX;
FMaxY:=MaxY;
FMinX:=MinX;
FMinY:=MinY;
finally
SL.Free;
msl.Free;
end;
end;
function TPolygon.PointInside(x, y:do
uble): Boolean;
function PointOnLine(x1,y1,x2,y2ouble):Boolean;
var
xx,yyouble;
begin
if ((x<x1) and (x<x2)) or ((y<y1) and (y<y2))
or ((x>x1) and (x>x2)) or ((y>y1) and (y>y2)) then
begin
Result:=false;
exit;
end;
xx:=x-x1;
yy:=y-y1;
x2:=x2-x1;
y2:=y2-y1;
if x2=0 then
Result:=xx=0 //在垂线上
else
if y2=0 then
Result:=yy=0 //在水平线上
else
Result:=xx*y2=yy*x2;
end;
function Between(f,f1,f2ouble):Boolean;
begin
if f1<f2 then
Result:=(f>=f1) and (f<=f2)
else
Result:=(f<=f1) and (f>=f2);
end;
function IntersectionDistance(x1,y1,x2,y2ouble)ouble;
begin
//获得直线与水平线的交点到待考查点的距离——带方向(右侧为正)
x1:=x1-x;
x2:=x2-x;
if x1=x2 then
begin
//垂直穿越
Result:=x1;
exit;
end;
y1:=y1-y;
y2:=y2-y;
Result:=x2+y2*(x2-x1)/(y1-y2);
end;
var
i,MinPoint,SameMinPoint:Integer;
MinDistance,tmpfouble;
x1,y1,x2,y2ouble;
begin
Result:=false;
if ((x<FMinX) or (x>FMaxX)) and ((y<FMinY) or (y>FMaxY)) then
exit;
//判断是否落在多边形的边上(包含顶点)
for i:=0 to FPointCount-2do
if PointOnLine(FData.X,FData.Y,FData[i+1].X,FData[i+1].Y) then
begin
Result:=true;
exit;
end;
MinDistance:=0;
MinPoint:=-1;
SameMinPoint:=-1;
for i:=0 to FPointCount-2do
if ((FData.X>=x) or (FData[i+1].X>=x)) and
(FData.Y<>FData[i+1].Y) then
//过滤整体位于左侧的线段以及水平线
if Between(y,FData.Y,FData[i+1].Y) then
begin
tmpf:=IntersectionDistance(FData.X,FData.Y,FData[i+1].X,FData[i+1].Y);
if tmpf>0 then
//因为点不在直线上,所以交点的距离不可能等于0
if MinPoint=-1 then
begin
MinPoint:=i;
MinDistance:=tmpf;
end
else
if tmpf<MinDistance then
//找到一个距离更小的
begin
MinPoint:=i;
SameMinPoint:=-1;
MinDistance:=tmpf;
end
else
if tmpf=MinDistance then
//遇到距离同样近的情况,记下前一个的位置
begin
SameMinPoint:=MinPoint;
MinPoint:=i;
end;
end;
if (MinPoint>=0) and (SameMinPoint<0) then
//有交点,且最近交点不是多边形的顶点
Result:=FData[MinPoint].Y>FData[MinPoint+1].Y //根据穿越方向进行判别
else
if SameMinPoint>=0 then
//有交点,但是是多边形的顶点
begin
if SameMinPoint>MinPoint then
begin
i:=SameMinPoint;
SameMinPoint:=MinPoint;
MinPoint:=i;
end;
y1:=FData[SameMinPoint].Y-FData[MinPoint].Y;
y2:=FData[MinPoint].Y-FData[MinPoint+1].Y;
//由于已经过滤了水平重合的线段...
if (y1>0) and (y2>0) then
//两条线都是自上而下穿越水平线
Result:=true
else
if (y1<0) and (y2<0) then
//两条线都是自下而上穿越水平线
Result:=false
else
begin
//两条直线有升有降——位于水平线的同一侧
x1:=FData[SameMinPoint].X-FData[MinPoint].X;
x2:=FData[MinPoint].X-FData[MinPoint+1].X;
Result:=x1*y2>x2*y1;
//根据两条直线的斜率大小进行判断
end;
end;
end;
procedure TPolygon.Put(Index: Integer;
const Value: TBorderRec);
begin
if (Index>=0) and (Index<FPointCount) then
FData[Index]:=Value
else
raise Exception.Create('数组访问越界!');
end;
给大家一组测试用数据:
0,-0.5
0,0
0.5,-1
1,0
1,-1
0,-2
1,-1.5
2,-3
1,-4
1.25,-3.5
1,-2.5
0,-2.5
-0.5,-3.5
-1,-2.5
-1.5,-3.1
-2,-2.5
-1.5,-2.8
-1,-1
0,-0.5
欢迎查错、提出改进意见!(分数可以另外给)