计算两个纬度和经度地理坐标之间的距离

178

我正在计算两个地理坐标之间的距离,我正在对比我的应用与其他3-4个应用。当我计算距离时,我往往会得到3.3英里的平均值,而其他应用程序则得到3.5英里。对于我正在进行的计算来说,这是一个很大的差异。是否有任何好的类库可用于计算距离?我在C#中像这样计算:

public static double Calculate(double sLatitude,double sLongitude, double eLatitude, 
                               double eLongitude)
{
    var radiansOverDegrees = (Math.PI / 180.0);

    var sLatitudeRadians = sLatitude * radiansOverDegrees;
    var sLongitudeRadians = sLongitude * radiansOverDegrees;
    var eLatitudeRadians = eLatitude * radiansOverDegrees;
    var eLongitudeRadians = eLongitude * radiansOverDegrees;

    var dLongitude = eLongitudeRadians - sLongitudeRadians;
    var dLatitude = eLatitudeRadians - sLatitudeRadians;

    var result1 = Math.Pow(Math.Sin(dLatitude / 2.0), 2.0) + 
                  Math.Cos(sLatitudeRadians) * Math.Cos(eLatitudeRadians) * 
                  Math.Pow(Math.Sin(dLongitude / 2.0), 2.0);

    // Using 3956 as the number of miles around the earth
    var result2 = 3956.0 * 2.0 * 
                  Math.Atan2(Math.Sqrt(result1), Math.Sqrt(1.0 - result1));

    return result2;
}

我可能做错了什么?我应该先把它计算成公里,然后再转换为英里吗?


1
地球平均半径 = 6,371公里 = 3958.76英里 - Mitch Wheat
1
https://dev59.com/BHVD5IYBdhLWcg3wTJrF - Mitch Wheat
这个应该放在gis.stackexchange.com上吧。 - Daniel Powell
它可能有,但我的问题更关注如何在Windows Phone上计算这个问题,这有点不同。公式是相同的,但像DistanceTo方法这样的新方法调用并不一定可用。 - Jason N. Gaylord
1
建议您将 pi/180 存储起来,这样就不必重复计算。 - Chris Caviness
感谢@ChrisCaviness。我已经进行了更新。 - Jason N. Gaylord
15个回答

363

已经存在于.NET Framework 4及更高版本中的GeoCoordinate类已经具有GetDistanceTo方法。

var sCoord = new GeoCoordinate(sLatitude, sLongitude);
var eCoord = new GeoCoordinate(eLatitude, eLongitude);

return sCoord.GetDistanceTo(eCoord);

距离以米为单位。

您需要引用System.Device。


1
我已经检查过了,设备的GeoCordinate有一个GetDistanceTo方法,这就是你所提到的(但不是你上面所写的)。没关系。我将测试一下,看看内置的计算是否更好。谢谢Nigel! - Jason N. Gaylord
1
我可能问了一个错误的问题,但结果是以什么单位呈现的?是英里还是公里?我找不到任何信息。 - Saeed Neamati
3
根据 https://msdn.microsoft.com/en-us/library/system.device.location.geocoordinate.getdistanceto(v=vs.110).aspx ,该函数返回的单位是米。 - Andy Butland
请问能将距离转换成公里吗? - Banwari Yadav
@BanwariYadav,您已经得到了以米为单位的结果,只需将结果除以1000即可得到以公里为单位的距离... - Jorn.Beyers
显示剩余5条评论

130

GetDistance是最佳解决方案,但在许多情况下我们无法使用此方法(例如:通用应用程序)

  • 伪代码算法用于计算两个坐标之间的距离:

    public static double DistanceTo(double lat1, double lon1, double lat2, double lon2, char unit = 'K')
    {
        double rlat1 = Math.PI*lat1/180;
        double rlat2 = Math.PI*lat2/180;
        double theta = lon1 - lon2;
        double rtheta = Math.PI*theta/180;
        double dist =
            Math.Sin(rlat1)*Math.Sin(rlat2) + Math.Cos(rlat1)*
            Math.Cos(rlat2)*Math.Cos(rtheta);
        dist = Math.Acos(dist);
        dist = dist*180/Math.PI;
        dist = dist*60*1.1515;
    
        switch (unit)
        {
            case 'K': //Kilometers -> default
                return dist*1.609344;
            case 'N': //Nautical Miles 
                return dist*0.8684;
            case 'M': //Miles
                return dist;
        }
    
        return dist;
    }
    
  • 实际世界中的C#实现,它利用了扩展方法

    用法:

    var distance = new Coordinates(48.672309, 15.695585)
                    .DistanceTo(
                        new Coordinates(48.237867, 16.389477),
                        UnitOfLength.Kilometers
                    );
    

    实施:

    public class Coordinates
    {
        public double Latitude { get; private set; }
        public double Longitude { get; private set; }
    
        public Coordinates(double latitude, double longitude)
        {
            Latitude = latitude;
            Longitude = longitude;
        }
    }
    public static class CoordinatesDistanceExtensions
    {
        public static double DistanceTo(this Coordinates baseCoordinates, Coordinates targetCoordinates)
        {
            return DistanceTo(baseCoordinates, targetCoordinates, UnitOfLength.Kilometers);
        }
    
        public static double DistanceTo(this Coordinates baseCoordinates, Coordinates targetCoordinates, UnitOfLength unitOfLength)
        {
            var baseRad = Math.PI * baseCoordinates.Latitude / 180;
            var targetRad = Math.PI * targetCoordinates.Latitude/ 180;
            var theta = baseCoordinates.Longitude - targetCoordinates.Longitude;
            var thetaRad = Math.PI * theta / 180;
    
            double dist =
                Math.Sin(baseRad) * Math.Sin(targetRad) + Math.Cos(baseRad) *
                Math.Cos(targetRad) * Math.Cos(thetaRad);
            dist = Math.Acos(dist);
    
            dist = dist * 180 / Math.PI;
            dist = dist * 60 * 1.1515;
    
            return unitOfLength.ConvertFromMiles(dist);
        }
    }
    
    public class UnitOfLength
    {
        public static UnitOfLength Kilometers = new UnitOfLength(1.609344);
        public static UnitOfLength NauticalMiles = new UnitOfLength(0.8684);
        public static UnitOfLength Miles = new UnitOfLength(1);
    
        private readonly double _fromMilesFactor;
    
        private UnitOfLength(double fromMilesFactor)
        {
            _fromMilesFactor = fromMilesFactor;
        }
    
        public double ConvertFromMiles(double input)
        {
            return input*_fromMilesFactor;
        }
    } 
    

1
你能提供用于这个微积分的公式或者关于哪一行代码的注释吗?如果我想直接得到以公里为单位的距离,而不必进行转换,我需要改变什么呢? - AlbertoFdzM
感谢您提供这个好的解决方案,我现在可以在我的桌面应用程序中使用它了。 - Jamshaid K.
在我的UWP应用程序中非常好用,因为我无法使用GeoCoordinate。 - Zach Green
2
计算结果准确率为95%。以下函数的准确率为100%: https://dev59.com/gmw15IYBdhLWcg3w1vSB#51839058 - Malek Tubaisaht

96

对于那些仍然不满意的人(比如我),这里提供了来自.NET框架GeoCoordinate类的原始代码,重构成一个独立的方法:

并且,对于那些仍然不满意的人(比如我),这里提供了来自.NET框架GeoCoordinate类的原始代码,重构成了一个独立的方法:

public double GetDistance(double longitude, double latitude, double otherLongitude, double otherLatitude)
{
    var d1 = latitude * (Math.PI / 180.0);
    var num1 = longitude * (Math.PI / 180.0);
    var d2 = otherLatitude * (Math.PI / 180.0);
    var num2 = otherLongitude * (Math.PI / 180.0) - num1;
    var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0);
    
    return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
}

17
很好的回答,我想指出得到的距离单位是米,正如官方文档中所述。 - Leviathan
4
可以进行一些小优化,或者为了更易读,可以预先计算出 pi/180 的值赋给 double oneDegree = Math.PI / 180.0; - brakeroo
2
@brakeroo 谢谢你的回复。我想保留答案原样,因为这是原始的.NET代码。当然,任何人都可以随意按照你的建议进行操作。 - Marc
1
非常好用,最简单的答案 ;) - matasoy
1
值得注意的是,提供的答案中的方法计算了大圆距离 :) - Daniël J.M. Hoffman
显示剩余3条评论

17

这是 JavaScript 版本的代码,各位大佬注意啦

function distanceTo(lat1, lon1, lat2, lon2, unit) {
      var rlat1 = Math.PI * lat1/180
      var rlat2 = Math.PI * lat2/180
      var rlon1 = Math.PI * lon1/180
      var rlon2 = Math.PI * lon2/180
      var theta = lon1-lon2
      var rtheta = Math.PI * theta/180
      var dist = Math.sin(rlat1) * Math.sin(rlat2) + Math.cos(rlat1) * Math.cos(rlat2) * Math.cos(rtheta);
      dist = Math.acos(dist)
      dist = dist * 180/Math.PI
      dist = dist * 60 * 1.1515
      if (unit=="K") { dist = dist * 1.609344 }
      if (unit=="N") { dist = dist * 0.8684 }
      return dist
}

你需要 rlon1rlon2 做什么? - knugie

10

对于那些正在使用 Xamarin 且无法访问 GeoCoordinate 类的用户,可以改用 Android Location 类:

public static double GetDistanceBetweenCoordinates (double lat1, double lng1, double lat2, double lng2) {
            var coords1 = new Location ("");
            coords1.Latitude = lat1;
            coords1.Longitude = lng1;
            var coords2 = new Location ("");
            coords2.Latitude = lat2;
            coords2.Longitude = lng2;
            return coords1.DistanceTo (coords2);
        }

9

虽然这是一个老问题,但是关于性能和优化的答案没有满足我的要求。

这里是我优化后的C#代码变体(以千米为距离单位,不包含变量和冗余计算,非常接近于Haversine公式的数学表达式)。https://en.wikipedia.org/wiki/Haversine_formula

灵感来自:https://rosettacode.org/wiki/Haversine_formula#C.23

public static class Haversine
{
    public static double Calculate(double lat1, double lon1, double lat2, double lon2)
    {
        double rad(double angle) => angle * 0.017453292519943295769236907684886127d; // = angle * Math.Pi / 180.0d
        double havf(double diff) => Math.Pow(Math.Sin(rad(diff) / 2d), 2); // = sin²(diff / 2)
        return 12745.6 * Math.Asin(Math.Sqrt(havf(lat2 - lat1) + Math.Cos(rad(lat1)) * Math.Cos(rad(lat2)) * havf(lon2 - lon1))); // earth radius 6.372,8‬km x 2 = 12745.6
    }
}

Haversine Formular from Wikipedia


如果你在寻求性能方面的答案,这绝对是一个很好的选择。我已经在数亿个点上运行过它。 - perkrihe

7

有一个名为GeoCoordinate的库支持以下平台:

  • Mono
  • .NET 4.5
  • .NET Core
  • Windows Phone 8.x
  • 通用 Windows 平台
  • Xamarin iOS
  • Xamarin Android

通过 NuGet 安装:

PM> Install-Package GeoCoordinate

使用方法:

GeoCoordinate pin1 = new GeoCoordinate(lat, lng);
GeoCoordinate pin2 = new GeoCoordinate(lat, lng);

double distanceBetween = pin1.GetDistanceTo(pin2);

两个坐标之间的距离,以为单位。


5
当CPU/数学计算能力受限时:
在某些情况下(比如我的工作中),计算能力很稀缺(例如没有浮点处理器,使用小型微控制器),一些三角函数可能需要耗费大量CPU时间(例如3000个以上的时钟周期)。因此,当我只需要一个近似值,特别是如果CPU不能长时间占用,我会使用以下方法来最小化CPU开销:
/**------------------------------------------------------------------------
 * \brief  Great Circle distance approximation in km over short distances.
 *
 * Can be off by as much as 10%.
 *
 * approx_distance_in_mi = sqrt(x * x + y * y)
 *
 * where x = 69.1 * (lat2 - lat1)
 * and y = 69.1 * (lon2 - lon1) * cos(lat1/57.3)
 *//*----------------------------------------------------------------------*/
double    ApproximateDisatanceBetweenTwoLatLonsInKm(
                  double lat1, double lon1,
                  double lat2, double lon2
                  ) {
    double  ldRadians, ldCosR, x, y;

    ldRadians = (lat1 / 57.3) * 0.017453292519943295769236907684886;
    ldCosR = cos(ldRadians);
    x = 69.1 * (lat2 - lat1);
    y = 69.1 * (lon2 - lon1) * ldCosR;

    return sqrt(x * x + y * y) * 1.609344;  /* Converts mi to km. */
}

感谢https://github.com/kristianmandrup/geo_vectors/blob/master/Distance%20calc%20notes.txt提供的帮助。


4

基于Elliot Wood的函数,如果有人对C语言函数感兴趣,这个函数是可行的...

#define SIM_Degree_to_Radian(x) ((float)x * 0.017453292F)
#define SIM_PI_VALUE                         (3.14159265359)

float GPS_Distance(float lat1, float lon1, float lat2, float lon2)
{
   float theta;
   float dist;

   theta = lon1 - lon2;

   lat1 = SIM_Degree_to_Radian(lat1);
   lat2 = SIM_Degree_to_Radian(lat2);
   theta = SIM_Degree_to_Radian(theta);

   dist = (sin(lat1) * sin(lat2)) + (cos(lat1) * cos(lat2) * cos(theta));
   dist = acos(dist);

//   dist = dist * 180.0 / SIM_PI_VALUE;
//   dist = dist * 60.0 * 1.1515;
//   /* Convert to km */
//   dist = dist * 1.609344;

   dist *= 6370.693486F;

   return (dist);
}

您可以将其更改为双倍。它以公里为单位返回值。

3

与基准测试结果的比较

你需要关注结果的变化,并且你需要基准测试。

我在这个页面上选了几个答案并加上一个未知的答案,将它们与我20年前编写的代码进行了比较。

我使用了这些坐标进行测试:32.9697,-96.80322和29.46786,-98.53506。

这是我的方法:

 public static double CalculateDistanceBetweenCoordinates(double fromLatitude, double fromLongitude,
    double toLatitude, double toLongitude)
{
    double x = 69.1 * (toLatitude - fromLatitude);
    double y = 69.1 * (toLongitude - fromLongitude) * Math.Cos(fromLatitude / 57.3);

    // Convert to KM by multiplying 1.609344
    return (Math.Sqrt(x * x + y * y) * 1.609344);
}

以下是以公里为单位的结果:
  • 422.73893139401383 // 我的代码 *我的代码和Yanga产生了相同的结果。
  • 421.6152868008663 // 由Leitner提供
  • 422.8787129776151 // 由JanW提供
  • 422.73893139401383 // 由Yanga提供
  • 422.7592707099537 // 未知来源
你可以看到除了Leitner的答案有大约1公里的偏差之外,它们都非常接近。我还检查了两个在线计算器,它们的答案分别是421.8和422.8,所以也有1公里的偏差。
以下是运行1,000,000次迭代的基准测试,使用的是BenchmarkDotNet 0.13.2(请注意,我省略了Marc的答案)。
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22000.978/21H2)

英特尔酷睿i7-8700 CPU 3.20GHz (Coffee Lake),1个CPU,12个逻辑核心和6个物理核心

.NET SDK=6.0.401

[主机] : .NET 6.0.9 (6.0.922.41905),X64 RyuJIT AVX2

DefaultJob : .NET 6.0.9 (6.0.922.41905),X64 RyuJIT AVX2

方法 平均时间 误差 标准差 比率 比率标准差 排名 分配的内存 内存分配比率
MyCode 239.6 微秒 2.56 微秒 2.14 微秒 1.00 0.00 1 - NA
其它 34,014.2 微秒 405.67 微秒 379.46 微秒 142.25 1.69 2 32 B NA
Leitner 46,749.4 微秒 390.52 微秒 346.19 微秒 195.23 2.50 3 44 B NA
Yanga 48,360.2 微秒 955.85 微秒 1,062.43 微秒 202.75 4.24 4 44 B NA
JanW 97,399.6 微秒 708.25 微秒 627.84 微秒 406.54 4.79 5 592 B NA

供参考,这是我发现的另一种方法:

 return 12742 * Math.Asin(Math.Sqrt(0.5 - Math.Cos((lat2 - lat1) * 0.017453292519943295) / 2 + Math.Cos(lat1 * 0.017453292519943295) * Math.Cos(lat2 * 0.017453292519943295) * (1 - Math.Cos((lon2 - lon1) * 0.017453292519943295)) / 2));

总之,根据距离计算方式的不同,结果会有所差异。但就性能而言,我的方法远远是最快的,并且不需要分配任何内存。

你测试Marc的方法出现的问题是,与其他方法相比,它期望先输入经度再输入纬度。这样做将产生423.12423319447634的结果。 - JimmiTh
干得好!速度提升了100倍以上!在CPU时间有限的情况下,这非常重要,尤其是在许多微控制器应用中! - V. Wheeler
@JimmiTh 谢谢。我一定把纬度和经度搞混了。 - Code Schaden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接