C# 射线算法:判断GPS点是否在车辆工作区域内

引言

在车载导航、电子围栏、车队管理等场景中,经常需要判断车辆当前的GPS坐标(纬度、经度)是否位于某个预先定义的工作区域内。工作区域通常由一系列GPS坐标点构成的多边形表示。由于地球是一个椭球体,直接使用平面直角坐标系中的射线算法会产生误差。本文将介绍两种适用于GPS坐标的改进算法:

  1. 平面近似法:将经纬度直接视为平面坐标,适用于小范围区域(如几公里内),实现简单、性能高。
  2. 球面精确法:基于球面几何,适用于任意范围区域,精度高,但计算稍复杂。

考虑到车辆工作区域通常不超过几十公里,平面近似法已能满足大部分需求。本文重点实现平面近似法,同时处理经度跨越±180°边界的特殊情况,并给出精度说明。

算法原理(平面近似法)

与平面直角坐标系中的射线法类似:从待判断的点出发,向正东方向(经度增加方向)发射一条射线,计算与多边形各边的交点个数。交点数为奇数表示点在内部,偶数表示在外部。

关键改进

  • 将GPS坐标中的经度视为 X,纬度视为 Y。
  • 处理多边形跨越 180° 经线的情况:将多边形分割或归一化到统一经度区间(例如 [0, 360))。
  • 处理点落在边界上的情况:直接返回 true。

经度跨越问题及解决方案

当多边形的经度范围跨越 -180°/180° 边界时(例如从 170° 到 -170°),直接使用射线法会导致错误。解决方法:将多边形所有点的经度加上 360°,使得连续区间不再跨越。具体步骤:

  1. 检测多边形的经度范围是否跨越 180° 经线。
  2. 若跨越,将所有经度小于 0 的点加上 360°,同时测试点也进行相同调整(若其经度 < 0,则加 360°)。
  3. 执行标准射线法。

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,建议使用球面算法。

球面精确算法(概要)

如需高精度判断,可采用球面多边形包含点算法

  1. 将经纬度转换为三维笛卡尔坐标 (x, y, z)。
  2. 使用绕数法或射线法,但射线需沿大圆弧计算,判断点是否在多边形内部。
  3. 常用库如 NetTopologySuite 支持地理坐标系。

以下是一个简化的球面射线法思路(不提供完整代码,供读者参考):

  • 将多边形各边视为大圆弧,计算从点出发的任意大圆弧与多边形边的交点个数。
  • 需处理数值稳定性问题,实现较为复杂。

总结

本文提供了基于平面近似法的GPS点包含判断算法,代码简洁高效,适用于车辆工作区域等中小尺度场景。同时处理了经度跨越180°边界的特殊情况。如需更高精度,可考虑球面算法或专业地理信息系统库。

完整代码已附上,可直接集成到您的C#项目中。欢迎评论区交流!


原创文章,转载请注明出处。

相关推荐
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章51-点查找
图像处理·人工智能·opencv·算法·计算机视觉
The Shio2 小时前
上位机对接设备协议踩坑指南
网络·单片机·嵌入式硬件·物联网·c#·.net
黎雁·泠崖2 小时前
二叉树遍历:LeetCode 144 / 94 / 145 之递归 + 分治 + 非递归
java·数据结构·算法·leetcode
弹简特2 小时前
【Linux命令饲养指南】Ubuntu 安装 MySQL【AI辅助实现】
linux·mysql·ubuntu
CompaqCV2 小时前
OpencvSharp 算子学习教案之 - Cv2.Add
学习·c#·opencvsharp算子
凌波粒2 小时前
LeetCode--347.前 K 个高频元素(栈和队列)
java·数据结构·算法·leetcode
FluxMelodySun2 小时前
机器学习(三十二) 半监督学习-基于分歧的方法与半监督聚类
人工智能·算法·机器学习
steem_ding2 小时前
C++ 回调函数详解
开发语言·c++·算法
会编程的土豆2 小时前
字符串知识(LCS,LIS)区分总结归纳
开发语言·数据结构·c++·算法