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

186

你好,我需要计算具有经纬度的两个点之间的距离。

我想避免调用外部API。

我尝试使用PHP实现Haversine公式:

下面是代码:

class CoordDistance
 {
    public $lat_a = 0;
    public $lon_a = 0;
    public $lat_b = 0;
    public $lon_b = 0;

    public $measure_unit = 'kilometers';

    public $measure_state = false;

    public $measure = 0;

    public $error = '';



    public function DistAB()

      {
          $delta_lat = $this->lat_b - $this->lat_a ;
          $delta_lon = $this->lon_b - $this->lon_a ;

          $earth_radius = 6372.795477598;

          $alpha    = $delta_lat/2;
          $beta     = $delta_lon/2;
          $a        = sin(deg2rad($alpha)) * sin(deg2rad($alpha)) + cos(deg2rad($this->lat_a)) * cos(deg2rad($this->lat_b)) * sin(deg2rad($beta)) * sin(deg2rad($beta)) ;
          $c        = asin(min(1, sqrt($a)));
          $distance = 2*$earth_radius * $c;
          $distance = round($distance, 4);

          $this->measure = $distance;

      }
    }

测试一些具有公共距离的给定点时,我得不到可靠结果。

我不明白是原始公式出错还是我的实现出了问题。


6
我在这里找到了很多语言的工作代码,包括php http://www.geodatasource.com/developers/php。 - krishna
13个回答

360

不久前,我写了一个Haversine公式的示例,并在我的网站上发布了它:

/**
 * Calculates the great-circle distance between two points, with
 * the Haversine formula.
 * @param float $latitudeFrom Latitude of start point in [deg decimal]
 * @param float $longitudeFrom Longitude of start point in [deg decimal]
 * @param float $latitudeTo Latitude of target point in [deg decimal]
 * @param float $longitudeTo Longitude of target point in [deg decimal]
 * @param float $earthRadius Mean earth radius in [m]
 * @return float Distance between points in [m] (same as earthRadius)
 */
function haversineGreatCircleDistance(
  $latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
{
  // convert from degrees to radians
  $latFrom = deg2rad($latitudeFrom);
  $lonFrom = deg2rad($longitudeFrom);
  $latTo = deg2rad($latitudeTo);
  $lonTo = deg2rad($longitudeTo);

  $latDelta = $latTo - $latFrom;
  $lonDelta = $lonTo - $lonFrom;

  $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
    cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
  return $angle * $earthRadius;
}

请注意,使用参数$earthRadius传入的单位和返回的距离单位相同。默认值为6371000米,因此结果也将以[m]表示。如果要得到英里作为单位的结果,可以将3959英里作为$earthRadius传递,结果将以[mi]表示。在我看来,如果没有特殊原因,坚持使用国际单位制是一种好习惯。

编辑:

正如TreyA所指出的那样,Haversine公式在处理对踵点时存在缺陷,这是由于舍入误差引起的(尽管对于小距离而言它是稳定的)。为了避免这些问题,您可以改用Vincenty公式

/**
 * Calculates the great-circle distance between two points, with
 * the Vincenty formula.
 * @param float $latitudeFrom Latitude of start point in [deg decimal]
 * @param float $longitudeFrom Longitude of start point in [deg decimal]
 * @param float $latitudeTo Latitude of target point in [deg decimal]
 * @param float $longitudeTo Longitude of target point in [deg decimal]
 * @param float $earthRadius Mean earth radius in [m]
 * @return float Distance between points in [m] (same as earthRadius)
 */
public static function vincentyGreatCircleDistance(
  $latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
{
  // convert from degrees to radians
  $latFrom = deg2rad($latitudeFrom);
  $lonFrom = deg2rad($longitudeFrom);
  $latTo = deg2rad($latitudeTo);
  $lonTo = deg2rad($longitudeTo);

  $lonDelta = $lonTo - $lonFrom;
  $a = pow(cos($latTo) * sin($lonDelta), 2) +
    pow(cos($latFrom) * sin($latTo) - sin($latFrom) * cos($latTo) * cos($lonDelta), 2);
  $b = sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lonDelta);

  $angle = atan2(sqrt($a), $b);
  return $angle * $earthRadius;
}

1
@TreyA - 可能有不同的版本,这个版本实现了Wikipedia中的公式,并经过了充分的测试。$angle表示世界中心的角度(以弧度为单位),因此可以将其乘以地球半径。如果有人感兴趣,我也可以提供更复杂的Vincenty公式的示例。 - martinstoeckli
1
@capikaw - 正如您在评论中看到的,单位是十进制度和米。实际上,您传递给参数 $earthRadius 的单位确定了结果的单位,这意味着,如果您将 km 作为地球半径传递,您将以 km 为单位获得距离。 - martinstoeckli
一个小距离有多大? - Salketer
@Salketer - 是的,这当然是相对的,一米可以既长又短,你需要更多地说明你的意图。如果你担心舍入误差,为什么要使用反余弦函数?你可以很容易地计算出 $angle 的舍入误差对距离的影响,只需将误差与地球半径相乘即可:$roundingError * $earthRadius - martinstoeckli
1
@PratikCJoshi - 终于找到时间添加有关使用不同单位的注释。 - martinstoeckli
显示剩余13条评论

81

我找到了这段代码,它给我提供了可靠的结果。

function distance($lat1, $lon1, $lat2, $lon2, $unit) {

  $theta = $lon1 - $lon2;
  $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) +  cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
  $dist = acos($dist);
  $dist = rad2deg($dist);
  $miles = $dist * 60 * 1.1515;
  $unit = strtoupper($unit);

  if ($unit == "K") {
      return ($miles * 1.609344);
  } else if ($unit == "N") {
      return ($miles * 0.8684);
  } else {
      return $miles;
  }
}

结果:

echo distance(32.9697, -96.80322, 29.46786, -98.53506, "M") . " Miles<br>";
echo distance(32.9697, -96.80322, 29.46786, -98.53506, "K") . " Kilometers<br>";
echo distance(32.9697, -96.80322, 29.46786, -98.53506, "N") . " Nautical Miles<br>";

3
太棒了!我试过了,谷歌地图也显示相同的距离,只是小数点有点差别。 - Zohair
如果你想计算三个点之间的距离,应该怎么做呢? - kexxcream
3
调用该函数两次并将它们相加,交替更改函数。 - Janith Chinthana
在某些情况下,返回NaN。https://dev59.com/_pbfa4cB1Zd3GeqP1fy7 - Zahur Sh

31

这只是对@martinstoeckli@Janith Chinthana回答的补充。对于好奇哪种算法最快的人,我编写了性能测试。最佳性能结果显示来自codexworld.com的优化函数:

/**
 * Optimized algorithm from http://www.codexworld.com
 *
 * @param float $latitudeFrom
 * @param float $longitudeFrom
 * @param float $latitudeTo
 * @param float $longitudeTo
 *
 * @return float [km]
 */
function codexworldGetDistanceOpt($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo)
{
    $rad = M_PI / 180;
    //Calculate distance from latitude and longitude
    $theta = $longitudeFrom - $longitudeTo;
    $dist = sin($latitudeFrom * $rad) 
        * sin($latitudeTo * $rad) +  cos($latitudeFrom * $rad)
        * cos($latitudeTo * $rad) * cos($theta * $rad);

    return acos($dist) / $rad * 60 *  1.853;
}

这是测试结果:

Test name       Repeats         Result          Performance     
codexworld-opt  10000           0.084952 sec    +0.00%
codexworld      10000           0.104127 sec    -22.57%
custom          10000           0.107419 sec    -26.45%
custom2         10000           0.111576 sec    -31.34%
custom1         10000           0.136691 sec    -60.90%
vincenty        10000           0.165881 sec    -95.26%

在你的代码中,Codexworld算法的乘数是1.852,而实际原始的乘数是1.1515。为什么会这样?有什么区别? - GotBatteries
@GotBatteries 原始的乘数是英里。优化后的函数返回公里结果。1.1515 * 1.609344 = 1.853。谢谢,已修正为1.853。 - Alexander Yancharuk
为什么不使用 M_PI / 180 和 $rad * 60 * 1.853 作为常量以获得更好的性能? - Evren Yurtesen
@EvrenYurtesen 如果你的优先考虑是性能,那么这是一个不错的想法。但我认为可维护性和可读性会变得更加复杂。 - Alexander Yancharuk
在上一行加上注释并写上 // M_PI / 180 ... 等等。我不知道为什么这会使维护变得困难。这不是你会改变的东西。 - Evren Yurtesen
显示剩余2条评论

16

这是一个相对比较老的问题,但对于那些希望使用PHP代码获得与谷歌地图相同结果的人来说,以下代码可以胜任:

/**
 * Computes the distance between two coordinates.
 *
 * Implementation based on reverse engineering of
 * <code>google.maps.geometry.spherical.computeDistanceBetween()</code>.
 *
 * @param float $lat1 Latitude from the first point.
 * @param float $lng1 Longitude from the first point.
 * @param float $lat2 Latitude from the second point.
 * @param float $lng2 Longitude from the second point.
 * @param float $radius (optional) Radius in meters.
 *
 * @return float Distance in meters.
 */
function computeDistance($lat1, $lng1, $lat2, $lng2, $radius = 6378137)
{
    static $x = M_PI / 180;
    $lat1 *= $x; $lng1 *= $x;
    $lat2 *= $x; $lng2 *= $x;
    $distance = 2 * asin(sqrt(pow(sin(($lat1 - $lat2) / 2), 2) + cos($lat1) * cos($lat2) * pow(sin(($lng1 - $lng2) / 2), 2)));

    return $distance * $radius;
}

我已经测试了各种坐标,它运行得非常完美。

我认为它应该比一些替代方案更快。但我没有测试过。

提示:谷歌地图使用6378137作为地球半径。因此,将其与其他算法一起使用也可能有效。


16

这里是计算两个纬度和经度之间距离的简单完美代码。以下代码来源于此 - http://www.codexworld.com/distance-between-two-addresses-google-maps-api-php/

$latitudeFrom = '22.574864';
$longitudeFrom = '88.437915';

$latitudeTo = '22.568662';
$longitudeTo = '88.431918';

//Calculate distance from latitude and longitude
$theta = $longitudeFrom - $longitudeTo;
$dist = sin(deg2rad($latitudeFrom)) * sin(deg2rad($latitudeTo)) +  cos(deg2rad($latitudeFrom)) * cos(deg2rad($latitudeTo)) * cos(deg2rad($theta));
$dist = acos($dist);
$dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515;

$distance = ($miles * 1.609344).' km';

8

对于喜欢更短、更快速的人(不使用deg2rad()函数)。

function circle_distance($lat1, $lon1, $lat2, $lon2) {
  $rad = M_PI / 180;
  return acos(sin($lat2*$rad) * sin($lat1*$rad) + cos($lat2*$rad) * cos($lat1*$rad) * cos($lon2*$rad - $lon1*$rad)) * 6371;// Kilometers
}

4

试用此函数计算两个经纬度点之间的距离。

function calculateDistanceBetweenTwoPoints($latitudeOne='', $longitudeOne='', $latitudeTwo='', $longitudeTwo='',$distanceUnit ='',$round=false,$decimalPoints='')
    {
        if (empty($decimalPoints)) 
        {
            $decimalPoints = '3';
        }
        if (empty($distanceUnit)) {
            $distanceUnit = 'KM';
        }
        $distanceUnit = strtolower($distanceUnit);
        $pointDifference = $longitudeOne - $longitudeTwo;
        $toSin = (sin(deg2rad($latitudeOne)) * sin(deg2rad($latitudeTwo))) + (cos(deg2rad($latitudeOne)) * cos(deg2rad($latitudeTwo)) * cos(deg2rad($pointDifference)));
        $toAcos = acos($toSin);
        $toRad2Deg = rad2deg($toAcos);

        $toMiles  =  $toRad2Deg * 60 * 1.1515;
        $toKilometers = $toMiles * 1.609344;
        $toNauticalMiles = $toMiles * 0.8684;
        $toMeters = $toKilometers * 1000;
        $toFeets = $toMiles * 5280;
        $toYards = $toFeets / 3;


              switch (strtoupper($distanceUnit)) 
              {
                  case 'ML'://miles
                         $toMiles  = ($round == true ? round($toMiles) : round($toMiles, $decimalPoints));
                         return $toMiles;
                      break;
                  case 'KM'://Kilometers
                        $toKilometers  = ($round == true ? round($toKilometers) : round($toKilometers, $decimalPoints));
                        return $toKilometers;
                      break;
                  case 'MT'://Meters
                        $toMeters  = ($round == true ? round($toMeters) : round($toMeters, $decimalPoints));
                        return $toMeters;
                      break;
                  case 'FT'://feets
                        $toFeets  = ($round == true ? round($toFeets) : round($toFeets, $decimalPoints));
                        return $toFeets;
                      break;
                  case 'YD'://yards
                        $toYards  = ($round == true ? round($toYards) : round($toYards, $decimalPoints));
                        return $toYards;
                      break;
                  case 'NM'://Nautical miles
                        $toNauticalMiles  = ($round == true ? round($toNauticalMiles) : round($toNauticalMiles, $decimalPoints));
                        return $toNauticalMiles;
                      break;
              }


    }

然后将该函数用作:

echo calculateDistanceBetweenTwoPoints('11.657740','77.766270','11.074820','77.002160','ML',true,5);

希望这可以帮到你


在我的实际情况下,经过验证的代码完美运行。 - Daxesh Vekariya
2
花了近5个小时来编写并在实际场景中验证它。 - ManojKiran A

3
尝试这个方法能够得到惊人的结果。
function getDistance($point1_lat, $point1_long, $point2_lat, $point2_long, $unit = 'km', $decimals = 2) {
        // Calculate the distance in degrees
        $degrees = rad2deg(acos((sin(deg2rad($point1_lat))*sin(deg2rad($point2_lat))) + (cos(deg2rad($point1_lat))*cos(deg2rad($point2_lat))*cos(deg2rad($point1_long-$point2_long)))));

        // Convert the distance in degrees to the chosen unit (kilometres, miles or nautical miles)
        switch($unit) {
            case 'km':
                $distance = $degrees * 111.13384; // 1 degree = 111.13384 km, based on the average diameter of the Earth (12,735 km)
                break;
            case 'mi':
                $distance = $degrees * 69.05482; // 1 degree = 69.05482 miles, based on the average diameter of the Earth (7,913.1 miles)
                break;
            case 'nmi':
                $distance =  $degrees * 59.97662; // 1 degree = 59.97662 nautic miles, based on the average diameter of the Earth (6,876.3 nautical miles)
        }
        return round($distance, $decimals);
    }

2

要获得精确值,请按照以下方式操作:

public function DistAB()
{
      $delta_lat = $this->lat_b - $this->lat_a ;
      $delta_lon = $this->lon_b - $this->lon_a ;

      $a = pow(sin($delta_lat/2), 2);
      $a += cos(deg2rad($this->lat_a9)) * cos(deg2rad($this->lat_b9)) * pow(sin(deg2rad($delta_lon/29)), 2);
      $c = 2 * atan2(sqrt($a), sqrt(1-$a));

      $distance = 2 * $earth_radius * $c;
      $distance = round($distance, 4);

      $this->measure = $distance;
}

嗯,我认为应该可以了...

编辑:

对于公式和至少JS实现,请尝试:http://www.movable-type.co.uk/scripts/latlong.html

敢问...我忘记在圆函数中将所有值转换为弧度了...


谢谢您的回答。我已经使用 $earth_radius = 6372.795477598 对 pointA(42,12) 和 pointB(43,12) 进行了简单的计算,结果为12745.591,而实际上应该是大约110.94左右。 - maxdangelo

1

由于大圆距离理论的原因,每个坐标的乘数都会发生变化,具体内容如下:

http://en.wikipedia.org/wiki/Great-circle_distance

你可以使用这里描述的公式来计算最近的值:

http://en.wikipedia.org/wiki/Great-circle_distance#Worked_example

关键是将每个度-分-秒值转换为所有度值:

N 36°7.2', W 86°40.2'  N = (+) , W = (-), S = (-), E = (+) 
referencing the Greenwich meridian and Equator parallel

(phi)     36.12° = 36° + 7.2'/60' 

(lambda)  -86.67° = 86° + 40.2'/60'

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