在C#中使用GMap.net绘制以两点经纬度为焦点的椭圆的技术咨询
刚好之前折腾过类似的需求,我来给你一步步拆解怎么实现——不管是用GMap.NET自带的工具,还是自己用公式手动算点,都给你讲清楚。
首先得明确一个关键前提:仅给定两个焦点的话,椭圆是不唯一的,因为椭圆的定义是“到两个焦点的距离之和为定值(也就是长轴长度2a)”,所以你得自己指定这个2a的值,而且必须保证2a大于两个焦点之间的距离,不然要么变成线段,要么根本不存在椭圆。
一、用GMap.NET实现的路子
GMap.NET本身没有直接画椭圆的控件,但我们可以用GMapPolygon来模拟——本质就是生成足够多的椭圆顶点,把这些点连起来就成了椭圆(点越多越平滑)。
具体步骤
先把基础参数定下来
先把两个焦点的经纬度转成GMap.NET的PointLatLng类型,再设定好长轴的一半a(记得a要大于两焦点距离的一半):// 示例:两个焦点的经纬度 var focus1 = new PointLatLng(39.9042, 116.4074); // 北京 var focus2 = new PointLatLng(31.2304, 121.4737); // 上海 // 设定长半轴a,这里我设成焦点距离的1.5倍,你可以根据需求改 var projection = gMapControl1.MapProvider.Projection; double distanceBetweenFoci = projection.GetDistance(focus1, focus2); double a = distanceBetweenFoci * 1.5;计算椭圆的核心参数
有了焦点和a,就能算出其他关键值:double c = distanceBetweenFoci / 2; // 焦点到中心的距离 double b = Math.Sqrt(Math.Pow(a, 2) - Math.Pow(c, 2)); // 短半轴 // 椭圆中心(两个焦点的中点) var center = new PointLatLng( (focus1.Lat + focus2.Lat) / 2, (focus1.Lng + focus2.Lng) / 2 ); // 计算长轴的旋转角度(焦点连线和x轴的夹角,用弧度) GPoint p1 = projection.FromLatLngToPixel(focus1, gMapControl1.Zoom); GPoint p2 = projection.FromLatLngToPixel(focus2, gMapControl1.Zoom); double angleRadians = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);生成椭圆的顶点集合
遍历0到2π的角度,每次生成一个点,这里我用100个点,足够平滑了:var ellipsePoints = new List<PointLatLng>(); int steps = 100; // 点的数量,越多越平滑 for (int i = 0; i < steps; i++) { double theta = 2 * Math.PI * i / steps; // 先算平面坐标下的椭圆点 double x = a * Math.Cos(theta); double y = b * Math.Sin(theta); // 把点旋转到长轴的方向 double rotatedX = x * Math.Cos(angleRadians) - y * Math.Sin(angleRadians); double rotatedY = x * Math.Sin(angleRadians) + y * Math.Cos(angleRadians); // 转回到经纬度 var centerPixel = projection.FromLatLngToPixel(center, gMapControl1.Zoom); var targetPixel = new GPoint(centerPixel.X + rotatedX, centerPixel.Y + rotatedY); var targetLatLng = projection.FromPixelToLatLng(targetPixel, gMapControl1.Zoom); ellipsePoints.Add(targetLatLng); } // 最后把第一个点加回去,让多边形闭合 ellipsePoints.Add(ellipsePoints[0]);把椭圆画到地图上
用GMapPolygon把点集合包装起来,加到地图的Overlay里就行:var ellipsePolygon = new GMapPolygon(ellipsePoints, "Focus-Based Ellipse"); ellipsePolygon.Fill = new SolidBrush(Color.FromArgb(50, Color.Blue)); // 半透明填充 ellipsePolygon.Stroke = new Pen(Color.Blue, 2); // 边框 gMapControl1.Overlays.Add(new GMapOverlay("EllipseOverlay") { ellipsePolygon });
二、不用GMap.NET内置函数的公式实现
如果不想依赖GMap.NET的投影工具,那核心就是把经纬度转成平面坐标,用平面椭圆的参数方程生成点,再转回到经纬度。适合需要完全自定义逻辑的场景。
核心公式逻辑
经纬度转局部平面坐标(墨卡托投影简化版)
小范围区域(几百公里内)用这个简化版足够,误差很小:void LatLngToLocalXY(PointLatLng latLng, PointLatLng center, out double x, out double y) { const double EarthRadius = 6378137; // 地球半径,单位米 double centerRadLat = center.Lat * Math.PI / 180; double radLat = latLng.Lat * Math.PI / 180; double radLng = latLng.Lng * Math.PI / 180; double centerRadLng = center.Lng * Math.PI / 180; x = EarthRadius * (radLng - centerRadLng) * Math.Cos(centerRadLat); y = EarthRadius * Math.Log(Math.Tan(Math.PI / 4 + radLat / 2)) - EarthRadius * Math.Log(Math.Tan(Math.PI / 4 + centerRadLat / 2)); }旋转后的平面椭圆参数方程
已知两个焦点的平面坐标(x1,y1)、(x2,y2),中心(cx, cy),长半轴a,短半轴b,旋转角度α(焦点连线和x轴的夹角),椭圆上任意点的坐标是:x = cx + a*cosθ*cosα - b*sinθ*sinα y = cy + a*cosθ*sinα + b*sinθ*cosα这里的
θ从0到2π,步长越小,点越密集。平面坐标转回经纬度
对应上面的转换函数,写个反向的:void LocalXYToLatLng(double x, double y, PointLatLng center, out PointLatLng latLng) { const double EarthRadius = 6378137; double centerRadLat = center.Lat * Math.PI / 180; double centerRadLng = center.Lng * Math.PI / 180; double radLng = (x / (EarthRadius * Math.Cos(centerRadLat))) + centerRadLng; double expY = Math.Exp((y + EarthRadius * Math.Log(Math.Tan(Math.PI / 4 + centerRadLat / 2))) / EarthRadius); double radLat = 2 * Math.Atan(expY) - Math.PI / 2; latLng = new PointLatLng(radLat * 180 / Math.PI, radLng * 180 / Math.PI); }完整流程
- 用
LatLngToLocalXY把两个焦点转成局部平面坐标 - 计算
c = 0.5 * sqrt((x2-x1)^2 + (y2-y1)^2),设定a > c,算出b = sqrt(a² - c²) - 算旋转角度
α = atan2(y2-y1, x2-x1) - 遍历
θ生成平面点,再用LocalXYToLatLng转回经纬度 - 最后把这些经纬度点用来绘制就行(比如还是用GMapPolygon,或者自己画)
- 用
一些注意点
- 如果是跨大区域的椭圆(比如跨半球),简化的墨卡托投影误差会变大,这时候可能需要用更精确的球面椭圆公式,但一般业务场景下简化版足够用。
- 一定要保证
a > c,不然生成的不是椭圆——a=c时会变成两个焦点之间的线段,a<c的话根本没有符合条件的点。 - 顶点数量
steps可以自己调,50到200个点之间,既能保证平滑,又不会影响性能。
内容的提问来源于stack exchange,提问作者Jorge

