C#实现高德坐标转WGS84及区域判断
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的转换是一个非线性过程,需要使用迭代逼近法。基本思路是:
-
假设一个WGS84坐标点
-
将其转换为GCJ-02坐标
-
计算与目标GCJ-02坐标的差值
-
调整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 性能优化建议
-
预处理多边形:对于固定不变的多边形区域,可以预先计算其边界框,先进行快速边界框判断
-
空间索引:对于大量区域判断,可以使用R树等空间索引结构加速查询
-
并行处理:对于大量坐标点的转换,可以使用并行计算提高效率
5.2 精度注意事项
-
迭代次数:坐标转换中的迭代次数影响精度和性能,需要根据实际需求调整
-
浮点数精度:地理坐标计算中需要注意浮点数精度问题,适当使用容差值
-
坐标顺序:多边形点的顺序(顺时针或逆时针)会影响区域判断结果
5.3 边界情况处理
-
点在边界上:算法需要正确处理点在多边形边界上的情况
-
凹多边形:算法需要正确处理凹多边形的情况
-
自交多边形:自交多边形的区域判断结果可能不符合预期
六、实际应用场景
-
电子围栏:判断车辆或人员是否进入或离开指定区域
-
地理围栏营销:向进入特定区域的用户推送营销信息
-
区域统计:统计特定区域内的设备数量或活动频率
-
地理权限控制:根据用户位置控制功能访问权限
七、总结
本文详细介绍了C#中实现高德坐标(GCJ-02)到WGS84坐标转换的方法,以及判断点是否在多边形区域内的算法。通过完整的代码示例和详细说明,为开发者提供了一套完整的解决方案。
关键点包括:
-
使用迭代逼近法实现GCJ-02到WGS84坐标的精确转换
-
使用射线法实现点与多边形的包含关系判断
-
提供了批量处理和区域判断的完整解决方案
-
讨论了性能优化和边界情况处理的注意事项
更多推荐
所有评论(0)