Pomiar odległości między dwoma współrzędnymi w PHP

145

Cześć {Hi} Mam potrzebę obliczenia odległości między dwoma punktami mającymi szerokie i długie.

Chciałbym uniknąć jakichkolwiek odwołań do zewnętrznego API.

Próbowałem zaimplementować formułę Haversine w PHP:

Oto kod:

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;

      }
    }

Testując to z niektórymi punktami, które mają publiczne odległości, nie otrzymuję wiarygodnego wyniku.

Nie rozumiem, czy w oryginalnej formule czy w mojej implementacji jest błąd

maxdangelo
źródło
4
Znalazłem działający kod w wielu językach, w tym php geodatasource.com/developers/php
krishna

Odpowiedzi:

273

Niedawno napisałem przykład formuły haversine i opublikowałem go na swojej stronie internetowej:

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

➽ Zwróć uwagę, że odległość otrzymujesz z powrotem w tej samej jednostce, jaką podajesz z parametrem $earthRadius. Wartość domyślna to 6371000 metrów, więc wynik również będzie w [m]. Aby otrzymać wynik w milach, możesz np. Przejechać 3959 mil jako, $earthRadiusa wynik będzie w [mi]. Moim zdaniem dobrym zwyczajem jest trzymanie się jednostek SI, jeśli nie ma powodu, aby postępować inaczej.

Edytować:

Jak słusznie zauważył TreyA, formuła Haversine ma słabe punkty z punktami antypodalnymi z powodu błędów zaokrąglania (chociaż jest stabilna dla małych odległości). Aby je ominąć, możesz zamiast tego użyć formuły Vincenty'ego .

/**
 * 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;
}
martinstoeckli
źródło
1
@TreyA - Możliwe są różne wersje, ta wersja implementuje formułę z Wikipedii i jest dobrze przetestowana. Kąt $ oznacza kąt w środku świata w radianach, więc można go pomnożyć przez promień Ziemi. Mogę też podać przykład bardziej złożonej formuły Vincenty'ego, jeśli ktoś jest zainteresowany.
martinstoeckli
@TreyA - Tak, wiem, nie jestem pewien, co chcesz przez to powiedzieć. Czy przetestowałeś funkcję i czy obliczyła nieprawidłowy wynik? Czy spojrzałeś na wzór w Wikipedii? Powinieneś naprawdę zrobić własny test i dać mi przykład tego, co Twoim zdaniem jest źle obliczone.
martinstoeckli
Przepraszam, ale muszę teraz wyjaśnić kilka rzeczy. 1) Pytanie dotyczyło formuły Haversine, powinieneś nam powiedzieć, jeśli sugerujesz użycie innej formuły. 2) Formuła Haversine ma słabe punkty wokół biegunów, ale jest dokładna dla małych odległości (jest to problem formuły arccosine). 3) Stwierdziłeś, że brakuje kroku w obliczonym kącie $, to jest po prostu błędne, nie może poprawić wyniku, prosimy o przetestowanie! 4) Zgadzam się, że lepiej byłoby zastosować stabilną formułę Vincenty, już zaproponowałem, że dam przykład. Może ty też mógłbyś napisać odpowiedź?
martinstoeckli
@martinstoekli - masz rację, nie brakuje Ci kroku w formule Haversine. Usunąłem swoje komentarze, aby nie zmylić przyszłych czytelników.
TreyA
1
@PratikCJoshi - Wreszcie znalazłem czas, aby dodać notatkę o używaniu różnych jednostek.
martinstoeckli
63

Znalazłem ten kod, który daje mi wiarygodne wyniki.

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

wyniki:

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>";
Janith Chinthana
źródło
2
świetna rzecz, próbowałem tego, a także mapy google pokazują tę samą odległość, tylko zmiany dziesiętne tu i tam ..
Zohair
A co jeśli chcesz obliczyć odległość między trzema punktami?
kexxcream
3
wywołaj tę funkcję dwa razy i dodaj je, naprzemiennie odpowiednio zmienisz funkcję
Janith Chinthana
zwraca NaN w pewnych warunkach stackoverflow.com/questions/37184259/…
Zahur Sh
23

To tylko dodatek do odpowiedzi @martinstoeckli i @Janith Chinthana . Dla ciekawskich, który algorytm jest najszybszy, napisałem test wydajności . Najlepszy wynik wydajności pokazuje zoptymalizowaną funkcję z 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;
}

Oto wyniki testu:

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%
Alexander Yancharuk
źródło
W twoim kodzie mnożnik dla algorytmów codexworlds wynosi 1,852, podczas gdy rzeczywisty oryginał to 1,1515. Dlaczego to? Skąd ta różnica?
GotBatteries
@GotBatteries Oryginalny multiplikator jest na mile. Zoptymalizowana funkcja zwraca wynik w km. 1.1515 * 1.609344 = 1.853. Dzięki, naprawiono do 1.853.
Alexander Yancharuk
Dlaczego nie używasz M_PI / 180 i $ rad * 60 * 1.853 jako stałych dla lepszej wydajności?
Evren Yurtesen,
@EvrenYurtesen Niezły pomysł, jeśli priorytetem jest wydajność. Ale wydaje mi się, że konserwacja i czytelność staną się bardziej skomplikowane.
Alexander Yancharuk
Po prostu umieść komentarz w poprzednim wierszu i powiedz // M_PI / 180 ... itd. Nie wiem, dlaczego miałoby to utrudniać utrzymanie. To nie jest coś, co kiedykolwiek zmienisz.
Evren Yurtesen,
10

Oto prosty i doskonały kod do obliczania odległości między dwiema szerokościami i długościami geograficznymi. Znaleziono następujący kod tutaj - 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';
JoyGuru
źródło
5

Dla tych, którzy lubią krótsze i szybsze (bez wywoływania 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
}
Semra
źródło
2

Spróbuj to daje niesamowite rezultaty

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);
    }
Amit
źródło
2

Dość stare pytanie, ale dla tych, którzy są zainteresowani kodem PHP, który zwraca takie same wyniki, jak Google Maps, następujące zadanie:

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

Testowałem z różnymi współrzędnymi i działa idealnie.

Myślę, że powinno być szybsze niż niektóre alternatywy. Ale tego nie przetestowałem.

Wskazówka: Google Maps używa 6378137 jako promienia Ziemi. Więc użycie go z innymi algorytmami może również działać.

Diego Andrade
źródło
1

Aby uzyskać dokładne wartości, zrób to w ten sposób:

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, myślę, że to powinno wystarczyć ...

Edytować:

W przypadku formularzy i przynajmniej implementacji JS spróbuj: http://www.movable-type.co.uk/scripts/latlong.html

Ośmiel mnie ... zapomniałem zdeg2radować wszystkie wartości w funkcjach koła ...

Legy
źródło
Dziękuję za odpowiedź. Sprawdziłem tę implementację za pomocą prostego obliczenia między punktem A (42,12) a punktemB (43,12) za pomocą $ earth_radius = 6372.795477598 Otrzymuję jako wynik 12745.591, kiedy powinno być coś około 110,94
maxdangelo
1

Witaj tutaj Kod, aby uzyskać odległość i czas za pomocą dwóch różnych szerokości i długości

$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);

Możesz sprawdzić przykład poniżej. Link pobiera czas między dwiema różnymi lokalizacjami, używając szerokości i długości geograficznej w php

Jydipsinh Parmar
źródło
6
Nie ma potrzeby wywoływania api w przypadku czegoś, co można po prostu znaleźć za pomocą matematyki.
Ivotje50
1

Wypróbuj tę funkcję, aby obliczyć odległość między punktami szerokości i długości geograficznej

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


    }

Następnie użyj funkcji jako

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

Mam nadzieję, że to pomoże

Manojkiran.A.
źródło
zweryfikowany z prawdziwym scenariuszem idealna praca w moim przypadku.
Daxesh Vekariya
1
Napisanie
0

Mnożnik jest zmieniany dla każdej współrzędnej z powodu teorii wielkiej odległości po okręgu, jak napisano tutaj:

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

i możesz obliczyć najbliższą wartość za pomocą tego wzoru opisanego tutaj:

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

klucz konwertuje wartość każdego stopnia - minuty - sekundy na wartość wszystkich stopni:

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'
Taha Paksu
źródło
0

Jednym z najłatwiejszych sposobów jest:

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

Zaokrągla do 2 miejsc po przecinku.

NAVNEET CHANDAN
źródło