引言
在车载导航、电子围栏、车队管理等场景中,经常需要判断车辆当前的GPS坐标(纬度、经度)是否位于某个预先定义的工作区域内。工作区域通常由一系列GPS坐标点构成的多边形表示。由于地球是一个椭球体,直接使用平面直角坐标系中的射线算法会产生误差。本文将介绍两种适用于GPS坐标的改进算法:
- 平面近似法:将经纬度直接视为平面坐标,适用于小范围区域(如几公里内),实现简单、性能高。
- 球面精确法:基于球面几何,适用于任意范围区域,精度高,但计算稍复杂。
考虑到车辆工作区域通常不超过几十公里,平面近似法已能满足大部分需求。本文重点实现平面近似法,同时处理经度跨越±180°边界的特殊情况,并给出精度说明。
算法原理(平面近似法)
与平面直角坐标系中的射线法类似:从待判断的点出发,向正东方向(经度增加方向)发射一条射线,计算与多边形各边的交点个数。交点数为奇数表示点在内部,偶数表示在外部。
关键改进:
- 将GPS坐标中的经度视为 X,纬度视为 Y。
- 处理多边形跨越 180° 经线的情况:将多边形分割或归一化到统一经度区间(例如 [0, 360))。
- 处理点落在边界上的情况:直接返回 true。
经度跨越问题及解决方案
当多边形的经度范围跨越 -180°/180° 边界时(例如从 170° 到 -170°),直接使用射线法会导致错误。解决方法:将多边形所有点的经度加上 360°,使得连续区间不再跨越。具体步骤:
- 检测多边形的经度范围是否跨越 180° 经线。
- 若跨越,将所有经度小于 0 的点加上 360°,同时测试点也进行相同调整(若其经度 < 0,则加 360°)。
- 执行标准射线法。
C# 实现
1. 定义 GPS 点结构
csharp
public struct GpsPoint
{
public double Lat { get; set; } // 纬度 -90 到 90
public double Lon { get; set; } // 经度 -180 到 180
public GpsPoint(double lat, double lon)
{
Lat = lat;
Lon = lon;
}
}
2. 核心判断类
csharp
using System;
using System.Collections.Generic;
public static class GpsPolygonHelper
{
/// <summary>
/// 判断GPS点是否在多边形内部(平面近似法,适用于小范围区域)
/// </summary>
/// <param name="point">待判断的GPS点</param>
/// <param name="polygon">多边形顶点列表(顺序排列)</param>
/// <returns>true: 在区域内或边界上; false: 在区域外</returns>
public static bool IsPointInPolygon(GpsPoint point, List<GpsPoint> polygon)
{
if (polygon == null || polygon.Count < 3)
return false;
// 处理经度跨越180度边界
var transformedPoints = NormalizePolygonAndPoint(polygon, point);
var normalizedPolygon = transformedPoints.Polygon;
var normalizedPoint = transformedPoints.Point;
int count = 0;
int vertexCount = normalizedPolygon.Count;
for (int i = 0; i < vertexCount; i++)
{
var p1 = normalizedPolygon[i];
var p2 = normalizedPolygon[(i + 1) % vertexCount];
// 检查点是否在线段上
if (IsPointOnSegment(normalizedPoint, p1, p2))
return true;
// 忽略水平边(纬度相同)
if (Math.Abs(p1.Lat - p2.Lat) < 1e-9)
continue;
// 判断射线(向正东)是否与边相交
bool isLatBetween = (p1.Lat > normalizedPoint.Lat) != (p2.Lat > normalizedPoint.Lat);
if (!isLatBetween)
continue;
// 计算交点经度(线性插值)
double intersectLon = p1.Lon + (normalizedPoint.Lat - p1.Lat) *
(p2.Lon - p1.Lon) / (p2.Lat - p1.Lat);
if (intersectLon > normalizedPoint.Lon)
count++;
}
return (count % 2) == 1;
}
/// <summary>
/// 处理经度跨越±180°的情况,将所有经度归一化到 [0, 360)
/// </summary>
private static (List<GpsPoint> Polygon, GpsPoint Point) NormalizePolygonAndPoint(
List<GpsPoint> polygon, GpsPoint point)
{
// 判断多边形是否需要归一化:检查是否存在经度跨越
bool needNormalize = false;
double minLon = double.MaxValue, maxLon = double.MinValue;
foreach (var p in polygon)
{
minLon = Math.Min(minLon, p.Lon);
maxLon = Math.Max(maxLon, p.Lon);
}
// 如果经度范围超过 180 度,很可能跨越了边界(简单启发式)
if (maxLon - minLon > 180.0)
needNormalize = true;
// 也可通过检查相邻点经度差是否大于 180 度来判断,这里采用范围法
List<GpsPoint> normalizedPolygon = new List<GpsPoint>();
GpsPoint normalizedPoint = point;
if (needNormalize)
{
foreach (var p in polygon)
{
double newLon = p.Lon;
if (newLon < 0) newLon += 360.0;
normalizedPolygon.Add(new GpsPoint(p.Lat, newLon));
}
if (normalizedPoint.Lon < 0)
normalizedPoint = new GpsPoint(normalizedPoint.Lat, normalizedPoint.Lon + 360.0);
}
else
{
normalizedPolygon = new List<GpsPoint>(polygon);
}
return (normalizedPolygon, normalizedPoint);
}
/// <summary>
/// 判断点是否在线段上(包括端点)
/// </summary>
private static bool IsPointOnSegment(GpsPoint p, GpsPoint a, GpsPoint b)
{
// 叉积判断共线
double cross = (p.Lon - a.Lon) * (b.Lat - a.Lat) - (p.Lat - a.Lat) * (b.Lon - a.Lon);
if (Math.Abs(cross) > 1e-9) return false;
// 点积判断投影范围
double dot = (p.Lon - a.Lon) * (b.Lon - a.Lon) + (p.Lat - a.Lat) * (b.Lat - a.Lat);
if (dot < 0) return false;
double squaredLen = (b.Lon - a.Lon) * (b.Lon - a.Lon) + (b.Lat - a.Lat) * (b.Lat - a.Lat);
if (dot > squaredLen) return false;
return true;
}
}
3. 控制台测试示例
csharp
class Program
{
static void Main()
{
// 定义工作区域(车辆电子围栏),坐标格式 (纬度, 经度)
List<GpsPoint> workZone = new List<GpsPoint>
{
new GpsPoint(31.2304, 121.4737), // 上海某区域
new GpsPoint(31.2400, 121.4800),
new GpsPoint(31.2350, 121.4950),
new GpsPoint(31.2200, 121.4900),
new GpsPoint(31.2220, 121.4750)
};
// 测试点
GpsPoint[] testPoints = {
new GpsPoint(31.2300, 121.4800), // 内部
new GpsPoint(31.2350, 121.4850), // 内部
new GpsPoint(31.2450, 121.4900), // 外部
new GpsPoint(31.2250, 121.4780) // 内部
};
foreach (var pt in testPoints)
{
bool inside = GpsPolygonHelper.IsPointInPolygon(pt, workZone);
Console.WriteLine($"GPS点 ({pt.Lat:F4}, {pt.Lon:F4}) : {(inside ? "在工作区域内" : "在区域外")}");
}
// 测试跨越180度经线的多边形(例如太平洋区域)
List<GpsPoint> crossingPolygon = new List<GpsPoint>
{
new GpsPoint(30, 170),
new GpsPoint(30, -170),
new GpsPoint(40, -170),
new GpsPoint(40, 170)
};
GpsPoint testCross = new GpsPoint(35, 180); // 在边界附近
bool result = GpsPolygonHelper.IsPointInPolygon(testCross, crossingPolygon);
Console.WriteLine($"\n跨越180°测试: 点(35,180) 结果: {(result ? "内部" : "外部")}");
}
}
输出示例:
GPS点 (31.2300, 121.4800) : 在工作区域内
GPS点 (31.2350, 121.4850) : 在工作区域内
GPS点 (31.2450, 121.4900) : 在区域外
GPS点 (31.2250, 121.4780) : 在工作区域内
跨越180°测试: 点(35,180) 结果: 内部
精度说明与改进建议
平面近似法的误差
将经纬度直接视为平面坐标会产生以下误差:
- 长度变形:经度1度对应的实际距离随纬度变化(赤道处约111km,高纬度处很小)。
- 面积变形:多边形面积和形状会失真。
- 适用范围:在纬度 ±60° 以内,区域边长小于 10km 时,误差通常在米级,对车辆区域判断可接受。若区域跨度超过 100km,建议使用球面算法。
球面精确算法(概要)
如需高精度判断,可采用球面多边形包含点算法:
- 将经纬度转换为三维笛卡尔坐标 (x, y, z)。
- 使用绕数法或射线法,但射线需沿大圆弧计算,判断点是否在多边形内部。
- 常用库如
NetTopologySuite支持地理坐标系。
以下是一个简化的球面射线法思路(不提供完整代码,供读者参考):
- 将多边形各边视为大圆弧,计算从点出发的任意大圆弧与多边形边的交点个数。
- 需处理数值稳定性问题,实现较为复杂。
总结
本文提供了基于平面近似法的GPS点包含判断算法,代码简洁高效,适用于车辆工作区域等中小尺度场景。同时处理了经度跨越180°边界的特殊情况。如需更高精度,可考虑球面算法或专业地理信息系统库。
完整代码已附上,可直接集成到您的C#项目中。欢迎评论区交流!
原创文章,转载请注明出处。