Medición de la distancia entre dos coordenadas en PHP


145

Hola, necesito calcular la distancia entre dos puntos que tienen el lat y el largo.

Me gustaría evitar cualquier llamada a API externa.

Intenté implementar la fórmula de Haversine en PHP:

Aquí está el código:

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;

      }
    }

Al probarlo con algunos puntos dados que tienen distancias públicas, no obtengo un resultado confiable.

No entiendo si hay un error en la fórmula original o en mi implementación


44
Encontré código de trabajo aquí en muchos idiomas, incluyendo php geodatasource.com/developers/php
krishna

Respuestas:


273

No hace mucho escribí un ejemplo de la fórmula de Haversine y la publiqué en mi sitio web:

/**
 * 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;
}

➽ Tenga en cuenta que obtiene la distancia de regreso en la misma unidad que pasa con el parámetro $earthRadius. El valor predeterminado es 6371000 metros, por lo que el resultado también estará en [m]. Para obtener el resultado en millas, podría pasar, por ejemplo, 3959 millas $earthRadiusy el resultado sería en [mi]. En mi opinión, es una buena costumbre quedarse con las unidades SI, si no hay una razón particular para hacerlo de otra manera.

Editar:

Como TreyA señaló correctamente, la fórmula de Haversine tiene debilidades con puntos antipodales debido a errores de redondeo (aunque es estable para distancias pequeñas). Para evitarlos, puedes usar la fórmula de Vincenty en su lugar.

/**
 * 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: hay diferentes versiones posibles, esta versión implementa la fórmula en Wikipedia y está bien probada. El ángulo $ significa el ángulo en el medio del mundo en radianes, por lo que uno puede multiplicarlo con el radio de la tierra. También puedo proporcionar un ejemplo de la fórmula más compleja de Vincenty si alguien está interesado.
martinstoeckli

@TreyA - Sí, lo sé, no estoy seguro de qué quieres decir con eso. ¿Ha probado la función y calculó un resultado incorrecto? ¿Y has mirado la fórmula en Wikipedia? Realmente deberías hacer tu propia prueba y darme un ejemplo de lo que crees que está mal calculado.
martinstoeckli

Lo siento, pero tengo que explicar algunas cosas ahora. 1) La pregunta era sobre la fórmula de Haversine, debe informarnos si sugiere utilizar otra fórmula. 2) La fórmula de Haversine tiene debilidades alrededor de los polos, pero es precisa para distancias pequeñas (es un problema de la fórmula de arcocosina). 3) Usted dijo que falta un paso con el ángulo calculado de $, eso es simplemente incorrecto, no puede mejorar el resultado, ¡por favor pruébelo! 4) Estoy de acuerdo en que sería mejor usar la fórmula estable de Vincenty, ya ofrecí dar un ejemplo. ¿Quizás podrías escribir una respuesta también?
martinstoeckli

@martinstoekli: tiene razón, no le falta un paso en su fórmula de Haversine. Eliminé mis comentarios para no confundir a los futuros lectores.
TreyA

1
@PratikCJoshi - Finalmente encontré el tiempo para agregar una nota sobre el uso de diferentes unidades.
martinstoeckli

63

Encontré este código que me está dando resultados confiables.

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;
  }
}

resultados:

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>";

2
cosas geniales, probé esto y también google maps muestra la misma distancia solo cambios decimales aquí y allá ...
Zohair

¿Qué pasa si quieres calcular la distancia entre tres puntos?
kexxcream

3
llame a esta función dos veces y agréguelas, alternativamente cambie la función en consecuencia
Janith Chinthana

devuelve NaN en algunas condiciones stackoverflow.com/questions/37184259/…
Zahur Sh

23

Es solo una adición a las respuestas de @martinstoeckli y @Janith Chinthana . Para aquellos que tienen curiosidad sobre qué algoritmo es más rápido, escribí la prueba de rendimiento . El mejor resultado de rendimiento muestra la función optimizada de 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;
}

Aquí están los resultados de la prueba:

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%

En su código, el multiplicador para los algoritmos de codexworlds es 1.852, mientras que el original real es 1.1515. ¿Por qué es esto? ¿Por qué la diferencia?
GotBatteries

@GotBatteries El mulitplier original es por millas. La función optimizada devuelve el resultado en km. 1.1515 * 1.609344 = 1.853. Gracias, arreglado a 1.853.
Alexander Yancharuk

¿Por qué no utiliza M_PI / 180 y $ rad * 60 * 1.853 como constantes para un mejor rendimiento?
Evren Yurtesen

@EvrenYurtesen Buena idea si su prioridad es el rendimiento. Pero la capacidad de mantenimiento y la legibilidad serán más complicadas, creo.
Alexander Yancharuk

Simplemente ponga un comentario en la línea anterior y diga // M_PI / 180 ... etc. No sé por qué dificultaría el mantenimiento. No es algo que puedas cambiar.
Evren Yurtesen

10

Aquí el código simple y perfecto para calcular la distancia entre dos latitudes y longitudes. El siguiente código se ha encontrado desde aquí: 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';

5

Para los que les gusta más corto y más rápido (sin llamar a 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
}

2

Prueba esto da resultados asombrosos

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

Pregunta bastante antigua, pero para aquellos interesados ​​en un código PHP que devuelva los mismos resultados que Google Maps, lo siguiente hace el trabajo:

/**
 * 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;
}

He probado con varias coordenadas y funciona perfectamente.

Creo que debería ser más rápido que algunas alternativas también. Pero no probé eso.

Sugerencia: Google Maps utiliza 6378137 como radio de la Tierra. Por lo tanto, usarlo con otros algoritmos también podría funcionar.


1

Para valores exactos hazlo así:

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;
}

Hmm, creo que debería hacerlo ...

Editar:

Para formuladores y al menos implementaciones JS intente: http://www.movable-type.co.uk/scripts/latlong.html

Atrévete ... Olvidé deg2rad todos los valores en las funciones de círculo ...


Gracias por tu respuesta. He comprobado esta implementación con un cálculo simple entre el punto A (42,12) y el punto B (43,12) usando $ earth_radius = 6372.795477598 Obtengo el resultado 12745.591 cuando debería ser alrededor de 110,94
maxdangelo

1

Hola aquí Código para obtener distancia y tiempo usando dos lat y long diferentes

$url ="https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&origins=16.538048,80.613266&destinations=23.0225,72.5714";



    $ch = curl_init();
    // Disable SSL verification

    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    // Will return the response, if false it print the response
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // Set the url
    curl_setopt($ch, CURLOPT_URL,$url);
    // Execute
    $result=curl_exec($ch);
    // Closing
    curl_close($ch);

    $result_array=json_decode($result);
print_r($result_array);

Puede verificar el ejemplo debajo del enlace para obtener tiempo entre dos ubicaciones diferentes usando la latitud y la longitud en php


66
Puede ser innecesario llamar a una API para algo que simplemente se puede encontrar usando las matemáticas.
Ivotje50

1

Pruebe esta función para calcular la distancia entre los puntos de latitud y longitud

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;
              }


    }

Luego use la función como

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

Espero eso ayude


verificado con escenario real trabajo perfecto en mi caso.
Daxesh Vekariya

1
Tomó casi 5 horas para escribir y verificar en escenario real
Manojkiran.A

0

El multiplicador se cambia en cada coordenada debido a la teoría de la distancia del gran círculo como se escribe aquí:

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

y puede calcular el valor más cercano utilizando esta fórmula que se describe aquí:

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

la clave es convertir cada valor de grado - minuto - segundo a todo el valor de grado:

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'

0

Una de las formas más fáciles es:

$my_latitude = "";
$my_longitude = "";
$her_latitude = "";
$her_longitude = "";

$distance = round((((acos(sin(($my_latitude*pi()/180)) * sin(($her_latitude*pi()/180))+cos(($my_latitude*pi()/180)) * cos(($her_latitude*pi()/180)) * cos((($my_longitude- $her_longitude)*pi()/180))))*180/pi())*60*1.1515*1.609344), 2);
echo $distance;

Redondeará hasta 2 puntos decimales.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.