C#实现高德坐标转WGS84及多边形区域判断完整指南

摘要

本文详细介绍了如何使用C#实现高德地图坐标系(GCJ-02)到WGS84坐标系的转换,以及如何判断转换后的GPS坐标是否在指定多边形区域内。通过完整的代码示例和算法解析,为开发者提供了一套完整的坐标转换与区域判断解决方案。

一、背景介绍

在GIS应用开发中,我们经常需要处理不同坐标系之间的转换问题。高德地图使用的是GCJ-02坐标系(火星坐标系),而GPS设备通常使用WGS84坐标系。这两种坐标系之间存在一定的偏移,需要进行转换才能正确使用。

此外,在许多实际应用场景中,我们需要判断某个坐标点是否位于指定的多边形区域内,如电子围栏、区域统计等。本文将结合这两个需求,提供完整的实现方案。

二、坐标系转换原理

2.1 坐标系简介

  • WGS84坐标系:国际标准坐标系,GPS设备使用的坐标系

  • GCJ-02坐标系:中国国家测绘局制定的坐标系,高德地图、腾讯地图等使用

  • BD-09坐标系:百度地图在GCJ-02基础上进行了二次加密

2.2 转换算法原理

GCJ-02到WGS84的转换是一个非线性过程,需要使用迭代逼近法。基本思路是:

  1. 假设一个WGS84坐标点

  2. 将其转换为GCJ-02坐标

  3. 计算与目标GCJ-02坐标的差值

  4. 调整WGS84坐标点,重复上述过程直到差值足够小

三、代码实现

3.1 坐标转换类实现

public class CoordinateConverter
{
    // 克拉索天斯基椭球体参数值
    private const double a = 6378245.0;
    // 第一偏心率
    private const double ee = 0.00669342162296594323;
    
    // WGS84转GCJ-02(火星坐标系)
    public static (double, double) Wgs84ToGcj02(double wgsLon, double wgsLat)
    {
        if (OutOfChina(wgsLat, wgsLon))
            return (wgsLon, wgsLat); // 不在国内不偏移

        double dLon = TransformLon(wgsLon - 105.0, wgsLat - 35.0);
        double dLat = TransformLat(wgsLon - 105.0, wgsLat - 35.0);

        double radLat = wgsLat / 180.0 * Math.PI;
        double magic = Math.Sin(radLat);
        magic = 1 - ee * magic * magic;
        double sqrtMagic = Math.Sqrt(magic);

        dLon = (dLon * 180.0) / (a / sqrtMagic * Math.Cos(radLat) * Math.PI);
        dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);

        return (wgsLon + dLon, wgsLat + dLat);
    }
    
    // GCJ-02转WGS84(逆向转换,迭代逼近)
    public static (double, double) Gcj02ToWgs84(double gcjLon, double gcjLat)
    {
        if (OutOfChina(gcjLat, gcjLon))
            return (gcjLon, gcjLat);

        double wgsLon = gcjLon;
        double wgsLat = gcjLat;
        double threshold = 1e-7;
        int maxIterations = 30;

        for (int i = 0; i < maxIterations; i++)
        {
            var (tmpGcjLon, tmpGcjLat) = Wgs84ToGcj02(wgsLon, wgsLat);
            double deltaLon = gcjLon - tmpGcjLon;
            double deltaLat = gcjLat - tmpGcjLat;

            wgsLon += deltaLon;
            wgsLat += deltaLat;

            if (Math.Abs(deltaLon) < threshold && Math.Abs(deltaLat) < threshold)
                break;
        }

        return (Math.Round(wgsLon, 6), Math.Round(wgsLat, 6));
    }
    
    // 判断是否在国内
    private static bool OutOfChina(double lat, double lon)
    {
        return !(lon >= 72.004 && lon <= 137.8347 && lat >= 0.8293 && lat <= 55.8271);
    }

    // 经度偏移计算
    private static double TransformLon(double x, double y)
    {
        double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.Sqrt(Math.Abs(x));
        ret += (20.0 * Math.Sin(6.0 * x * Math.PI) + 20.0 * Math.Sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.Sin(x * Math.PI) + 40.0 * Math.Sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
        ret += (150.0 * Math.Sin(x / 12.0 * Math.PI) + 300.0 * Math.Sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
        return ret;
    }

    // 纬度偏移计算
    private static double TransformLat(double x, double y)
    {
        double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.Sqrt(Math.Abs(x));
        ret += (20.0 * Math.Sin(6.0 * x * Math.PI) + 20.0 * Math.Sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.Sin(y * Math.PI) + 40.0 * Math.Sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
        ret += (160.0 * Math.Sin(y / 12.0 * Math.PI) + 320.0 * Math.Sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
        return ret;
    }
}

3.2 点结构体和区域判断实现

// 定义一个点结构体
public struct Point
{
    public double Latitude;
    public double Longitude;
    public Point(double lat, double lon)
    {
        Latitude = lat;
        Longitude = lon;
    }
}

public class RegionChecker
{
    // 判断一个点是否在多边形内
    public static bool IsPointInPolygon(Point testPoint, List<Point> polygon)
    {
        const double epsilon = 1e-6;
        int n = polygon.Count;
        if (n < 3) return false;

        // 先检查是否在边界上
        for (int i = 0, j = n - 1; i < n; j = i++)
        {
            if (IsPointOnSegment(testPoint, polygon[i], polygon[j], epsilon))
                return true;
        }

        bool inside = false;

        for (int i = 0, j = n - 1; i < n; j = i++)
        {
            Point p1 = polygon[i];
            Point p2 = polygon[j];

            double xi = p1.Longitude, yi = p1.Latitude;
            double xj = p2.Longitude, yj = p2.Latitude;

            bool intersect = ((yi > testPoint.Latitude) != (yj > testPoint.Latitude))
                && (testPoint.Longitude < (xj - xi) * (testPoint.Latitude - yi) / (yj - yi + epsilon) + xi + epsilon);

            if (intersect)
                inside = !inside;
        }

        return inside;
    }

    // 检查点是否在线段上
    private static bool IsPointOnSegment(Point p, Point a, Point b, double epsilon)
    {
        // 检查是否在包围盒内
        if (p.Longitude < Math.Min(a.Longitude, b.Longitude) - epsilon ||
            p.Longitude > Math.Max(a.Longitude, b.Longitude) + epsilon ||
            p.Latitude < Math.Min(a.Latitude, b.Latitude) - epsilon ||
            p.Latitude > Math.Max(a.Latitude, b.Latitude) + epsilon)
            return false;

        // 共线检查(叉积接近0)
        double cross = (b.Longitude - a.Longitude) * (p.Latitude - a.Latitude)
                     - (b.Latitude - a.Latitude) * (p.Longitude - a.Longitude);

        if (Math.Abs(cross) > epsilon)
            return false;

        // 参数化检查
        double dot = (p.Longitude - a.Longitude) * (b.Longitude - a.Longitude)
                   + (p.Latitude - a.Latitude) * (b.Latitude - a.Latitude);
        if (dot < -epsilon)
            return false;

        double squaredLength = (b.Longitude - a.Longitude) * (b.Longitude - a.Longitude)
                            + (b.Latitude - a.Latitude) * (b.Latitude - a.Latitude);
        if (dot > squaredLength + epsilon)
            return false;

        return true;
    }
}

3.3 批量转换与区域判断实现

// 批量转换坐标并判断是否在区域内
public static string ConvertAndCheckInRegions(string gcj02Coordinates, List<List<Point>> regions)
{
    string result = "";
    var areaArray = gcj02Coordinates.Split(new[] { "||" }, StringSplitOptions.RemoveEmptyEntries);
    
    foreach (var area in areaArray)
    {
        var pointArray = area.Split(';');
        foreach (var pointStr in pointArray)
        {
            var coordArray = pointStr.Split(',');
            if (coordArray.Length == 2 && 
                double.TryParse(coordArray[0], out double lng) && 
                double.TryParse(coordArray[1], out double lat))
            {
                // 转换坐标
                var wgs84Point = CoordinateConverter.Gcj02ToWgs84(lng, lat);
                Point testPoint = new Point(wgs84Point.Item2, wgs84Point.Item1);
                
                // 判断是否在任一区域内
                bool inAnyRegion = false;
                foreach (var region in regions)
                {
                    if (RegionChecker.IsPointInPolygon(testPoint, region))
                    {
                        inAnyRegion = true;
                        break;
                    }
                }
                
                // 添加结果标记
                result += $"{wgs84Point.Item1},{wgs84Point.Item2}{(inAnyRegion ? ",in" : ",out")};";
            }
        }
        result = result.TrimEnd(';') + "|";
    }
    
    return result.TrimEnd('|');
}

四、使用示例

4.1 简单使用示例

// 单个坐标转换
var wgs84Point = CoordinateConverter.Gcj02ToWgs84(116.397428, 39.90923);
Console.WriteLine($"WGS84坐标: {wgs84Point.Item1}, {wgs84Point.Item2}");

// 创建多边形区域
List<Point> polygon = new List<Point>
{
    new Point(39.9, 116.4),
    new Point(39.9, 116.5),
    new Point(39.8, 116.5),
    new Point(39.8, 116.4)
};

// 判断点是否在多边形内
Point testPoint = new Point(39.85, 116.45);
bool isInside = RegionChecker.IsPointInPolygon(testPoint, polygon);
Console.WriteLine($"点是否在多边形内: {isInside}");

4.2 批量处理示例

// 输入格式: "经度1,纬度1;经度2,纬度2||经度3,纬度3;经度4,纬度4"
string inputCoordinates = "116.397428,39.90923;116.407428,39.91923||116.497428,39.95923;116.507428,39.96923";

// 定义多个区域
List<List<Point>> regions = new List<List<Point>>
{
    new List<Point> // 区域1
    {
        new Point(39.9, 116.4),
        new Point(39.9, 116.5),
        new Point(39.8, 116.5),
        new Point(39.8, 116.4)
    },
    new List<Point> // 区域2
    {
        new Point(39.96, 116.49),
        new Point(39.96, 116.51),
        new Point(39.94, 116.51),
        new Point(39.94, 116.49)
    }
};

// 批量转换并判断
string result = ConvertAndCheckInRegions(inputCoordinates, regions);
Console.WriteLine($"转换和判断结果: {result}");

五、算法优化与注意事项

5.1 性能优化建议

  1. 预处理多边形:对于固定不变的多边形区域,可以预先计算其边界框,先进行快速边界框判断

  2. 空间索引:对于大量区域判断,可以使用R树等空间索引结构加速查询

  3. 并行处理:对于大量坐标点的转换,可以使用并行计算提高效率

5.2 精度注意事项

  1. 迭代次数:坐标转换中的迭代次数影响精度和性能,需要根据实际需求调整

  2. 浮点数精度:地理坐标计算中需要注意浮点数精度问题,适当使用容差值

  3. 坐标顺序:多边形点的顺序(顺时针或逆时针)会影响区域判断结果

5.3 边界情况处理

  1. 点在边界上:算法需要正确处理点在多边形边界上的情况

  2. 凹多边形:算法需要正确处理凹多边形的情况

  3. 自交多边形:自交多边形的区域判断结果可能不符合预期

六、实际应用场景

  1. 电子围栏:判断车辆或人员是否进入或离开指定区域

  2. 地理围栏营销:向进入特定区域的用户推送营销信息

  3. 区域统计:统计特定区域内的设备数量或活动频率

  4. 地理权限控制:根据用户位置控制功能访问权限

七、总结

本文详细介绍了C#中实现高德坐标(GCJ-02)到WGS84坐标转换的方法,以及判断点是否在多边形区域内的算法。通过完整的代码示例和详细说明,为开发者提供了一套完整的解决方案。

关键点包括:

  1. 使用迭代逼近法实现GCJ-02到WGS84坐标的精确转换

  2. 使用射线法实现点与多边形的包含关系判断

  3. 提供了批量处理和区域判断的完整解决方案

  4. 讨论了性能优化和边界情况处理的注意事项

更多推荐