计算两个经纬度点之间的距离? (Haversine 公式)

Posted

技术标签:

【中文标题】计算两个经纬度点之间的距离? (Haversine 公式)【英文标题】:Calculate distance between two latitude-longitude points? (Haversine formula) 【发布时间】:2010-09-06 21:00:41 【问题描述】:

如何计算经纬度指定的两点之间的距离?

为了澄清,我想以公里为单位;这些点使用 WGS84 系统,我想了解可用方法的相对准确性。

【问题讨论】:

为了更准确 - 请参阅***.com/questions/1420045/… 请注意,您不能在像 WGS 84 这样的旋转椭球体上应用半正弦公式。您只能在具有半径的球体上应用此方法。 这里的大多数答案都是使用简单的球面三角法,因此与 GPS 系统中使用的 WGS84 椭球距离相比,结果相当粗糙。一些答案确实参考了 Vincenty 的椭球公式,但该算法是为 1960 年代的台式计算器设计的,并且存在稳定性和准确性问题;我们现在有更好的硬件和软件。请参阅GeographicLib 以了解具有多种语言实现的高质量库。 @MikeT - 虽然这里的许多答案似乎在小距离内很有用:如果您从 WGS 84 获取 lat/long,并应用 Haversine 就好像那些 是球体上的点,你没有得到答案,其错误只是由于地球的扁平化因素,所以可能在更准确公式的 1% 以内?需要注意的是,这些距离很短,比如在一个城镇内。 对于这些平台:Mono/.NET 4.5/.NET Core/Windows Phone 8.x/通用 Windows 平台/Xamarin ios/Xamarin android 参见***.com/a/54296314/2736742 【参考方案1】:

这是另一个转换为 Ruby 的代码:

include Math
#Note: from/to = [lat, long]

def get_distance_in_km(from, to)
  radians = lambda  |deg| deg * Math.PI / 180 
  radius = 6371 # Radius of the earth in kilometer
  dLat = radians[to[0]-from[0]]
  dLon = radians[to[1]-from[1]]

  cosines_product = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(radians[from[0]]) * Math.cos(radians[to[1]]) * Math.sin(dLon/2) * Math.sin(dLon/2)

  c = 2 * Math.atan2(Math.sqrt(cosines_product), Math.sqrt(1-cosines_product)) 
  return radius * c # Distance in kilometer
end

【讨论】:

【参考方案2】:

由于这是该主题最受欢迎的讨论,我将在此处添加我从 2019 年末至 2020 年初的经验。添加到现有答案 - 我的重点是找到一个准确且快速(即矢量化)的解决方案。

让我们从这里的答案最常用的方法开始 - Haversine 方法。向量化很简单,见下面的python示例:

def haversine(lat1, lon1, lat2, lon2):
    """
    Calculate the great circle distance between two points
    on the earth (specified in decimal degrees)

    All args must be of equal length.
    Distances are in meters.
    
    Ref:
    https://***.com/questions/29545704/fast-haversine-approximation-python-pandas
    https://ipython.readthedocs.io/en/stable/interactive/magics.html
    """
    Radius = 6.371e6
    lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])

    dlon = lon2 - lon1
    dlat = lat2 - lat1

    a = np.sin(dlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2.0)**2

    c = 2 * np.arcsin(np.sqrt(a))
    s12 = Radius * c
    
    # initial azimuth in degrees
    y = np.sin(lon2-lon1) * np.cos(lat2)
    x = np.cos(lat1)*np.sin(lat2) - np.sin(lat1)*np.cos(lat2)*np.cos(dlon)
    azi1 = np.arctan2(y, x)*180./math.pi

    return 's12':s12, 'azi1': azi1

在准确性方面,它是最不准确的。***指出平均 0.5% 的相对偏差没有任何来源。我的实验表明偏差较小。下面是 100,000 个随机点与我的库的比较,应该精确到毫米级:

np.random.seed(42)
lats1 = np.random.uniform(-90,90,100000)
lons1 = np.random.uniform(-180,180,100000)
lats2 = np.random.uniform(-90,90,100000)
lons2 = np.random.uniform(-180,180,100000)
r1 = inverse(lats1, lons1, lats2, lons2)
r2 = haversine(lats1, lons1, lats2, lons2)
print("Max absolute error: :4.2fm".format(np.max(r1['s12']-r2['s12'])))
print("Mean absolute error: :4.2fm".format(np.mean(r1['s12']-r2['s12'])))
print("Max relative error: :4.2f%".format(np.max((r2['s12']/r1['s12']-1)*100)))
print("Mean relative error: :4.2f%".format(np.mean((r2['s12']/r1['s12']-1)*100)))

输出:

Max absolute error: 26671.47m
Mean absolute error: -2499.84m
Max relative error: 0.55%
Mean relative error: -0.02%

因此,在 100,000 个随机坐标对上平均有 2.5 公里的偏差,这可能对大多数情况都有好处。

下一个选项是 Vincenty 的公式,该公式精确到毫米,具体取决于收敛标准,并且还可以矢量化。它确实存在在对跖点附近收敛的问题。您可以通过放宽收敛标准使其在这些点收敛,但准确度会下降到 0.25% 甚至更多。在对映点之外,Vincenty 将提供接近 Geographiclib 的结果,平均相对误差小于 1.e-6。

这里提到的 Geographiclib 确实是当前的黄金标准。它有几个实现并且相当快,尤其是如果您使用的是 C++ 版本。

现在,如果您打算将 Python 用于超过 10k 点的任何内容,我建议您考虑我的矢量化实现。我根据自己的需要创建了一个带有矢量化 Vincenty 例程的 geovectorslib 库,它使用 Geographiclib 作为近对跖点的后备。下面是 100k 点与 Geographiclib 的比较。如您所见,它为 100k 点提供了 20 倍的逆向改进和 100 倍的直接改进,并且差距会随着点数的增加而增长。精度方面,它将在 Georgraphiclib 的 1.e-5 rtol 内。

Direct method for 100,000 points
94.9 ms ± 25 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
9.79 s ± 1.4 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

Inverse method for 100,000 points
1.5 s ± 504 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
24.2 s ± 3.91 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

【讨论】:

【参考方案3】:

计算距离(尤其是大距离)的主要挑战之一是考虑地球的曲率。如果地球是平的,计算两点之间的距离就像计算直线一样简单! Haversine 公式包括一个表示地球半径的常数(它是下面的 R 变量)。根据您是以英里还是公里为单位,它分别等于 3956 英里或 6367 公里。 基本公式为:

dlon = lon2 - lon1
dlat = lat2 - lat1
a = (sin(dlat/2))^2 + cos(lat1) * cos(lat2) * (sin(dlon/2))^2
c = 2 * atan2( sqrt(a), sqrt(1-a) )
distance = R * c (where R is the radius of the Earth)
 
R = 6367 km OR 3956 mi
     lat1, lon1: The Latitude and Longitude of point 1 (in decimal degrees)
     lat2, lon2: The Latitude and Longitude of point 2 (in decimal degrees)
     unit: The unit of measurement in which to calculate the results where:
     'M' is statute miles (default)
     'K' is kilometers
     'N' is nautical miles

样本

function distance(lat1, lon1, lat2, lon2, unit) 
    try 
        var radlat1 = Math.PI * lat1 / 180
        var radlat2 = Math.PI * lat2 / 180
        var theta = lon1 - lon2
        var radtheta = Math.PI * theta / 180
        var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
        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
     catch (err) 
        console.log(err);
    

【讨论】:

虽然此代码可能会解决问题,including an explanation 关于如何以及为什么解决问题将真正有助于提高您的帖子质量,并可能导致更多的赞成票。请记住,您正在为将来的读者回答问题,而不仅仅是现在提问的人。请edit您的回答添加解释并说明适用的限制和假设。【参考方案4】:

如果您想要行驶距离/路线(在此处发布,因为这是谷歌上两点之间距离的第一个结果,但对于大多数人来说行驶距离更有用),您可以使用Google Maps Distance Matrix Service:

getDrivingDistanceBetweenTwoLatLong(origin, destination) 

 return new Observable(subscriber => 
  let service = new google.maps.DistanceMatrixService();
  service.getDistanceMatrix(
    
      origins: [new google.maps.LatLng(origin.lat, origin.long)],
      destinations: [new google.maps.LatLng(destination.lat, destination.long)],
      travelMode: 'DRIVING'
    , (response, status) => 
      if (status !== google.maps.DistanceMatrixStatus.OK) 
        console.log('Error:', status);
        subscriber.error(error: status, status: status);
       else 
        console.log(response);
        try 
          let valueInMeters = response.rows[0].elements[0].distance.value;
          let valueInKms = valueInMeters / 1000;
          subscriber.next(valueInKms);
          subscriber.complete();
        
       catch(error) 
        subscriber.error(error: error, status: status);
       
      
    );
);

【讨论】:

【参考方案5】:
function getDistanceFromLatLonInKm(position1, position2) 
    "use strict";
    var deg2rad = function (deg)  return deg * (Math.PI / 180); ,
        R = 6371,
        dLat = deg2rad(position2.lat - position1.lat),
        dLng = deg2rad(position2.lng - position1.lng),
        a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
            + Math.cos(deg2rad(position1.lat))
            * Math.cos(deg2rad(position2.lat))
            * Math.sin(dLng / 2) * Math.sin(dLng / 2),
        c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;


console.log(getDistanceFromLatLonInKm(
    lat: 48.7931459, lng: 1.9483572,
    lat: 48.827167, lng: 2.2459745
));

【讨论】:

你打错了,第二个cos应该是pos2【参考方案6】:

LUA 中的 math.deg 有问题...如果有人知道修复方法,请清理此代码!

与此同时,这里是 LUA 中 Haversine 的实现(与 Redis 一起使用!)

function calcDist(lat1, lon1, lat2, lon2)
    lat1= lat1*0.0174532925
    lat2= lat2*0.0174532925
    lon1= lon1*0.0174532925
    lon2= lon2*0.0174532925

    dlon = lon2-lon1
    dlat = lat2-lat1

    a = math.pow(math.sin(dlat/2),2) + math.cos(lat1) * math.cos(lat2) * math.pow(math.sin(dlon/2),2)
    c = 2 * math.asin(math.sqrt(a))
    dist = 6371 * c      -- multiply by 0.621371 to convert to miles
    return dist
end

干杯!

【讨论】:

不要忘记将变量 (dlon,dlat,a,c,dist) 声明为local,以免污染全局空间。【参考方案7】:

FSharp 版本,使用里程:

let radialDistanceHaversine location1 location2 : float = 
                let degreeToRadian degrees = degrees * System.Math.PI / 180.0
                let earthRadius = 3959.0
                let deltaLat = location2.Latitude - location1.Latitude |> degreeToRadian
                let deltaLong = location2.Longitude - location1.Longitude |> degreeToRadian
                let a =
                    (deltaLat / 2.0 |> sin) ** 2.0
                    + (location1.Latitude |> degreeToRadian |> cos)
                    * (location2.Latitude |> degreeToRadian |> cos)
                    * (deltaLong / 2.0 |> sin) ** 2.0
                atan2 (a |> sqrt) (1.0 - a |> sqrt)
                * 2.0
                * earthRadius

【讨论】:

【参考方案8】:

飞镖语言:

import 'dart:math' show cos, sqrt, asin;

double calculateDistance(LatLng l1, LatLng l2) 
  const p = 0.017453292519943295;
  final a = 0.5 -
      cos((l2.latitude - l1.latitude) * p) / 2 +
      cos(l1.latitude * p) *
          cos(l2.latitude * p) *
          (1 - cos((l2.longitude - l1.longitude) * p)) /
          2;
  return 12742 * asin(sqrt(a));

【讨论】:

【参考方案9】:

准确计算经纬度点之间的距离所需的函数很复杂,陷阱也很多。由于存在很大的不准确性(地球不是完美的球体),我不会推荐半正弦或其他球形解决方案。 vincenty formula 更好,但在某些情况下会抛出错误,即使编码正确。

我建议不要自己编写函数,而是使用 geopy,它已经实现了非常准确的 geographiclib 用于距离计算 (paper from author)。

#pip install geopy
from geopy.distance import geodesic
NY = [40.71278,-74.00594]
Beijing = [39.90421,116.40739]
print("WGS84: ",geodesic(NY, Beijing).km) #WGS84 is Standard
print("Intl24: ",geodesic(NY, Beijing, ellipsoid='Intl 1924').km) #geopy includes different ellipsoids
print("Custom ellipsoid: ",geodesic(NY, Beijing, ellipsoid=(6377., 6356., 1 / 297.)).km) #custom ellipsoid

#supported ellipsoids:
#model             major (km)   minor (km)     flattening
#'WGS-84':        (6378.137,    6356.7523142,  1 / 298.257223563)
#'GRS-80':        (6378.137,    6356.7523141,  1 / 298.257222101)
#'Airy (1830)':   (6377.563396, 6356.256909,   1 / 299.3249646)
#'Intl 1924':     (6378.388,    6356.911946,   1 / 297.0)
#'Clarke (1880)': (6378.249145, 6356.51486955, 1 / 293.465)
#'GRS-67':        (6378.1600,   6356.774719,   1 / 298.25)

这个库的唯一缺点是它不支持向量化计算。 对于矢量化计算,您可以使用新的geovectorslib。

#pip install geovectorslib
from geovectorslib import inverse
print(inverse(lats1,lons1,lats2,lons2)['s12'])

lats 和 lons 是 numpy 数组。 Geovectorslib 非常准确且速度极快!不过,我还没有找到改变椭球的解决方案。 WGS84 椭球作为标准使用,这是大多数用途的最佳选择。

【讨论】:

【参考方案10】:

这是 Erlang 的实现

lat_lng(Lat1, Lon1=_Point1, Lat2, Lon2=_Point2) ->
  P = math:pi() / 180,
  R = 6371, % Radius of Earth in KM
  A = 0.5 - math:cos((Lat2 - Lat1) * P) / 2 +
    math:cos(Lat1 * P) * math:cos(Lat2 * P) * (1 - math:cos((Lon2 - Lon1) * P))/2,
  R * 2 * math:asin(math:sqrt(A)).

【讨论】:

【参考方案11】:

这是一个简单的 javascript 函数,可能对 link.. 有用,但我们使用的是 google earth javascript 插件而不是地图

function getApproximateDistanceUnits(point1, point2) 

    var xs = 0;
    var ys = 0;

    xs = point2.getX() - point1.getX();
    xs = xs * xs;

    ys = point2.getY() - point1.getY();
    ys = ys * ys;

    return Math.sqrt(xs + ys);

单位不是距离,而是相对于坐标的比率。还有其他相关的计算可以替代 getApproximateDistanceUnits 函数link here

然后我用这个函数来查看一个纬度经度是否在半径之内

function isMapPlacemarkInRadius(point1, point2, radi) 
    if (point1 && point2) 
        return getApproximateDistanceUnits(point1, point2) <= radi;
     else 
        return 0;
    

点可以定义为

 $$.getPoint = function(lati, longi) 
        var location = 
            x: 0,
            y: 0,
            getX: function()  return location.x; ,
            getY: function()  return location.y; 
        ;
        location.x = lati;
        location.y = longi;

        return location;
    ;

然后你可以做你的事情来看看一个点是否在一个半径区域内:

 //put it on the map if within the range of a specified radi assuming 100,000,000 units
        var iconpoint = Map.getPoint(pp.latitude, pp.longitude);
        var centerpoint = Map.getPoint(Settings.CenterLatitude, Settings.CenterLongitude);

        //approx ~200 units to show only half of the globe from the default center radius
        if (isMapPlacemarkInRadius(centerpoint, iconpoint, 120)) 
            addPlacemark(pp.latitude, pp.longitude, pp.name);
        
        else 
            otherSidePlacemarks.push(
                latitude: pp.latitude,
                longitude: pp.longitude,
                name: pp.name
            );

        

【讨论】:

【参考方案12】:
//JAVA
    public Double getDistanceBetweenTwoPoints(Double latitude1, Double longitude1, Double latitude2, Double longitude2) 
    final int RADIUS_EARTH = 6371;

    double dLat = getRad(latitude2 - latitude1);
    double dLong = getRad(longitude2 - longitude1);

    double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(getRad(latitude1)) * Math.cos(getRad(latitude2)) * Math.sin(dLong / 2) * Math.sin(dLong / 2);
    double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return (RADIUS_EARTH * c) * 1000;
    

    private Double getRad(Double x) 
    return x * Math.PI / 180;
    

【讨论】:

【参考方案13】:

我创建了这个小的 Javascript LatLng 对象,可能对某人有用。

var latLng1 = new LatLng(5, 3);
var latLng2 = new LatLng(6, 7);
var distance = latLng1.distanceTo(latLng2); 

代码:

/**
 * latLng point
 * @param Number lat
 * @param Number lng
 * @returns LatLng
 * @constructor
 */
function LatLng(lat,lng) 
    this.lat = parseFloat(lat);
    this.lng = parseFloat(lng);

    this.__cache = ;


LatLng.prototype = 
    toString: function() 
        return [this.lat, this.lng].join(",");
    ,

    /**
     * calculate distance in km to another latLng, with caching
     * @param LatLng latLng
     * @returns Number distance in km
     */
    distanceTo: function(latLng) 
        var cacheKey = latLng.toString();
        if(cacheKey in this.__cache) 
            return this.__cache[cacheKey];
        

        // the fastest way to calculate the distance, according to this jsperf test;
        // http://jsperf.com/haversine-salvador/8
        // http://***.com/questions/27928
        var deg2rad = 0.017453292519943295; // === Math.PI / 180
        var lat1 = this.lat * deg2rad;
        var lng1 = this.lng * deg2rad;
        var lat2 = latLng.lat * deg2rad;
        var lng2 = latLng.lng * deg2rad;
        var a = (
            (1 - Math.cos(lat2 - lat1)) +
            (1 - Math.cos(lng2 - lng1)) * Math.cos(lat1) * Math.cos(lat2)
            ) / 2;
        var distance = 12742 * Math.asin(Math.sqrt(a)); // Diameter of the earth in km (2 * 6371)

        // cache the distance
        this.__cache[cacheKey] = distance;

        return distance;
    
;

【讨论】:

【参考方案14】:

您可以使用Haversine公式计算它:

a = sin²(Δφ/2) + cos φ1 ⋅ cos φ2 ⋅ sin²(Δλ/2)
c = 2 ⋅ atan2( √a, √(1−a) )
d = R ⋅ c

下面给出了一个计算两点之间距离的例子

假设我必须计算新德里到伦敦之间的距离,那么我该如何使用这个公式:

New delhi co-ordinates= 28.7041° N, 77.1025° E
London co-ordinates= 51.5074° N, 0.1278° W

var R = 6371e3; // metres
var φ1 = 28.7041.toRadians();
var φ2 = 51.5074.toRadians();
var Δφ = (51.5074-28.7041).toRadians();
var Δλ = (0.1278-77.1025).toRadians();

var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
        Math.cos(φ1) * Math.cos(φ2) *
        Math.sin(Δλ/2) * Math.sin(Δλ/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

var d = R * c; // metres
d = d/1000; // km

【讨论】:

【参考方案15】:

如果你使用的是python; 点安装geopy

from geopy.distance import geodesic


origin = (30.172705, 31.526725)  # (latitude, longitude) don't confuse
destination = (30.288281, 31.732326)

print(geodesic(origin, destination).meters)  # 23576.805481751613
print(geodesic(origin, destination).kilometers)  # 23.576805481751613
print(geodesic(origin, destination).miles)  # 14.64994773134371

【讨论】:

【参考方案16】:
function distance($lat1, $lon1, $lat2, $lon2)  
    $pi80 = M_PI / 180; 
    $lat1 *= $pi80; $lon1 *= $pi80; $lat2 *= $pi80; $lon2 *= $pi80; 
    $dlat = $lat2 - $lat1; 
    $dlon = $lon2 - $lon1; 
    $a = sin($dlat / 2) * sin($dlat / 2) + cos($lat1) * cos($lat2) * sin($dlon / 2) * sin($dlon / 2);  
    $km = 6372.797 * 2 * atan2(sqrt($a), sqrt(1 - $a)); 
    return $km; 

【讨论】:

【参考方案17】:

您也可以使用 geolib 之类的模块:

如何安装:

$ npm install geolib

使用方法:

import  getDistance  from 'geolib'

const distance = getDistance(
     latitude: 51.5103, longitude: 7.49347 ,
     latitude: "51° 31' N", longitude: "7° 28' E" 
)

console.log(distance)

文档: https://www.npmjs.com/package/geolib

【讨论】:

【参考方案18】:

这是一个 Scala 实现:

  def calculateHaversineDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double = 
    val long2 = lon2 * math.Pi / 180
    val lat2 = lat2 * math.Pi / 180
    val long1 = lon1 * math.Pi / 180
    val lat1 = lat1 * math.Pi / 180

    val dlon = long2 - long1
    val dlat = lat2 - lat1
    val a = math.pow(math.sin(dlat / 2), 2) + math.cos(lat1) * math.cos(lat2) * math.pow(math.sin(dlon / 2), 2)
    val c = 2 * math.atan2(Math.sqrt(a), math.sqrt(1 - a))
    val haversineDistance = 3961 * c // 3961 = radius of earth in miles
    haversineDistance
  

【讨论】:

以上是关于计算两个经纬度点之间的距离? (Haversine 公式)的主要内容,如果未能解决你的问题,请参考以下文章

计算两个latitude-longitude点之间的距离? (Haversine公式)

Haversine 公式 - 数学略有偏差,不确定原因

对haversine公式使用列表推导

在PHP中测量两个坐标之间的距离

计算两个纬度/经度点之间的距离不一致

使用python计算谷歌地图中2点之间的距离