Distance du grand cercle MySQL (formule Haversine)

184

J'ai un script PHP fonctionnel qui obtient les valeurs de longitude et de latitude, puis les saisit dans une requête MySQL. Je voudrais en faire uniquement MySQL. Voici mon code PHP actuel:

if ($distance != "Any" && $customer_zip != "") { //get the great circle distance

    //get the origin zip code info
    $zip_sql = "SELECT * FROM zip_code WHERE zip_code = '$customer_zip'";
    $result = mysql_query($zip_sql);
    $row = mysql_fetch_array($result);
    $origin_lat = $row['lat'];
    $origin_lon = $row['lon'];

    //get the range
    $lat_range = $distance/69.172;
    $lon_range = abs($distance/(cos($details[0]) * 69.172));
    $min_lat = number_format($origin_lat - $lat_range, "4", ".", "");
    $max_lat = number_format($origin_lat + $lat_range, "4", ".", "");
    $min_lon = number_format($origin_lon - $lon_range, "4", ".", "");
    $max_lon = number_format($origin_lon + $lon_range, "4", ".", "");
    $sql .= "lat BETWEEN '$min_lat' AND '$max_lat' AND lon BETWEEN '$min_lon' AND '$max_lon' AND ";
    }

Est-ce que quelqu'un sait comment faire cela entièrement MySQL? J'ai un peu navigué sur Internet, mais la plupart de la littérature à ce sujet est assez déroutante.

Nick Woodhams
la source
4
Sur la base de toutes les excellentes réponses ci-dessous, voici un exemple de travail de la formule Haversine en action
StartupGuy
Merci d'avoir partagé cela Michael.M
Nick Woodhams
stackoverflow.com/a/40272394/1281385 A un exemple de la façon de s'assurer que l'index est atteint
Exussum

Réponses:

357

De Google Code FAQ - Création d'un localisateur de magasin avec PHP, MySQL et Google Maps :

Voici l'instruction SQL qui trouvera les 20 emplacements les plus proches dans un rayon de 25 miles de la coordonnée 37, -122. Il calcule la distance en fonction de la latitude / longitude de cette ligne et de la latitude / longitude cible, puis demande uniquement les lignes où la valeur de distance est inférieure à 25, classe l'ensemble de la requête par distance et la limite à 20 résultats. Pour rechercher en kilomètres au lieu de miles, remplacez 3959 par 6371.

SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) 
* cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin(radians(lat)) ) ) AS distance 
FROM markers 
HAVING distance < 25 
ORDER BY distance 
LIMIT 0 , 20;
Pavel Chuchuva
la source
2
l'instruction SQL est vraiment bonne. mais où puis-je passer mes coordonnées dans cette déclaration? Je ne vois nulle part où les coordonnées sont passées
Mann
32
Remplacez 37 et -122 par vos coordonnées.
Pavel Chuchuva
5
Je m'interroge sur les implications de cela pour les performances s'il y a des millions d'endroits (+ des milliers de visiteurs) ...
Halil Özgür
12
Vous pouvez affiner la requête pour de meilleures performances comme expliqué dans ce document: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas
2
@FosAvance Oui, cette requête fonctionnerait si vous avez une markerstable avec des champs id, lan et lng.
Pavel Chuchuva
32

$greatCircleDistance = acos( cos($latitude0) * cos($latitude1) * cos($longitude0 - $longitude1) + sin($latitude0) * sin($latitude1));

avec latitude et longitude en radian.

alors

SELECT 
  acos( 
      cos(radians( $latitude0 ))
    * cos(radians( $latitude1 ))
    * cos(radians( $longitude0 ) - radians( $longitude1 ))
    + sin(radians( $latitude0 )) 
    * sin(radians( $latitude1 ))
  ) AS greatCircleDistance 
 FROM yourTable;

est votre requête SQL

pour obtenir vos résultats en Km ou miles, multipliez le résultat par le rayon moyen de la Terre ( 3959miles, 6371Km ou 3440miles nautiques)

La chose que vous calculez dans votre exemple est une boîte englobante. Si vous placez vos données de coordonnées dans une colonne MySQL à activation spatiale , vous pouvez utiliser la fonctionnalité intégrée de MySQL pour interroger les données.

SELECT 
  id
FROM spatialEnabledTable
WHERE 
  MBRWithin(ogc_point, GeomFromText('Polygon((0 0,0 3,3 3,3 0,0 0))'))
Jacco
la source
13

Si vous ajoutez des champs d'assistance à la table de coordonnées, vous pouvez améliorer le temps de réponse de la requête.

Comme ça:

CREATE TABLE `Coordinates` (
`id` INT(10) UNSIGNED NOT NULL COMMENT 'id for the object',
`type` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'type',
`sin_lat` FLOAT NOT NULL COMMENT 'sin(lat) in radians',
`cos_cos` FLOAT NOT NULL COMMENT 'cos(lat)*cos(lon) in radians',
`cos_sin` FLOAT NOT NULL COMMENT 'cos(lat)*sin(lon) in radians',
`lat` FLOAT NOT NULL COMMENT 'latitude in degrees',
`lon` FLOAT NOT NULL COMMENT 'longitude in degrees',
INDEX `lat_lon_idx` (`lat`, `lon`)
)    

Si vous utilisez TokuDB, vous obtiendrez des performances encore meilleures si vous ajoutez des index de clustering sur l'un des prédicats, par exemple, comme ceci:

alter table Coordinates add clustering index c_lat(lat);
alter table Coordinates add clustering index c_lon(lon);

Vous aurez besoin des lat et lon de base en degrés ainsi que sin (lat) en radians, cos (lat) * cos (lon) en radians et cos (lat) * sin (lon) en radians pour chaque point. Ensuite, vous créez une fonction mysql, comme ceci:

CREATE FUNCTION `geodistance`(`sin_lat1` FLOAT,
                              `cos_cos1` FLOAT, `cos_sin1` FLOAT,
                              `sin_lat2` FLOAT,
                              `cos_cos2` FLOAT, `cos_sin2` FLOAT)
    RETURNS float
    LANGUAGE SQL
    DETERMINISTIC
    CONTAINS SQL
    SQL SECURITY INVOKER
   BEGIN
   RETURN acos(sin_lat1*sin_lat2 + cos_cos1*cos_cos2 + cos_sin1*cos_sin2);
   END

Cela vous donne la distance.

N'oubliez pas d'ajouter un index sur lat / lon pour que la boîte englobante puisse aider la recherche au lieu de la ralentir (l'index est déjà ajouté dans la requête CREATE TABLE ci-dessus).

INDEX `lat_lon_idx` (`lat`, `lon`)

Étant donné une ancienne table avec uniquement des coordonnées lat / lon, vous pouvez configurer un script pour le mettre à jour comme ceci: (php en utilisant meekrodb)

$users = DB::query('SELECT id,lat,lon FROM Old_Coordinates');

foreach ($users as $user)
{
  $lat_rad = deg2rad($user['lat']);
  $lon_rad = deg2rad($user['lon']);

  DB::replace('Coordinates', array(
    'object_id' => $user['id'],
    'object_type' => 0,
    'sin_lat' => sin($lat_rad),
    'cos_cos' => cos($lat_rad)*cos($lon_rad),
    'cos_sin' => cos($lat_rad)*sin($lon_rad),
    'lat' => $user['lat'],
    'lon' => $user['lon']
  ));
}

Ensuite, vous optimisez la requête réelle pour ne faire le calcul de la distance que lorsque cela est vraiment nécessaire, par exemple en délimitant le cercle (enfin, ovale) de l'intérieur et de l'extérieur. Pour cela, vous devrez précalculer plusieurs métriques pour la requête elle-même:

// assuming the search center coordinates are $lat and $lon in degrees
// and radius in km is given in $distance
$lat_rad = deg2rad($lat);
$lon_rad = deg2rad($lon);
$R = 6371; // earth's radius, km
$distance_rad = $distance/$R;
$distance_rad_plus = $distance_rad * 1.06; // ovality error for outer bounding box
$dist_deg_lat = rad2deg($distance_rad_plus); //outer bounding box
$dist_deg_lon = rad2deg($distance_rad_plus/cos(deg2rad($lat)));
$dist_deg_lat_small = rad2deg($distance_rad/sqrt(2)); //inner bounding box
$dist_deg_lon_small = rad2deg($distance_rad/cos(deg2rad($lat))/sqrt(2));

Compte tenu de ces préparatifs, la requête ressemble à ceci (php):

$neighbors = DB::query("SELECT id, type, lat, lon,
       geodistance(sin_lat,cos_cos,cos_sin,%d,%d,%d) as distance
       FROM Coordinates WHERE
       lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d
       HAVING (lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d) OR distance <= %d",
  // center radian values: sin_lat, cos_cos, cos_sin
       sin($lat_rad),cos($lat_rad)*cos($lon_rad),cos($lat_rad)*sin($lon_rad),
  // min_lat, max_lat, min_lon, max_lon for the outside box
       $lat-$dist_deg_lat,$lat+$dist_deg_lat,
       $lon-$dist_deg_lon,$lon+$dist_deg_lon,
  // min_lat, max_lat, min_lon, max_lon for the inside box
       $lat-$dist_deg_lat_small,$lat+$dist_deg_lat_small,
       $lon-$dist_deg_lon_small,$lon+$dist_deg_lon_small,
  // distance in radians
       $distance_rad);

EXPLAIN sur la requête ci-dessus peut indiquer qu'il n'utilise pas d'index à moins qu'il n'y ait suffisamment de résultats pour déclencher un tel. L'index sera utilisé lorsqu'il y aura suffisamment de données dans la table de coordonnées. Vous pouvez ajouter FORCE INDEX (lat_lon_idx) au SELECT pour lui faire utiliser l'index sans tenir compte de la taille de la table, afin que vous puissiez vérifier avec EXPLAIN qu'il fonctionne correctement.

Avec les exemples de code ci-dessus, vous devriez avoir une implémentation fonctionnelle et évolutive de la recherche d'objets par distance avec une erreur minimale.

silvio
la source
10

J'ai dû régler cela en détail, je vais donc partager mon résultat. Cela utilise une ziptable avec latitudeet longitudetables. Cela ne dépend pas de Google Maps; vous pouvez plutôt l'adapter à n'importe quelle table contenant lat / long.

SELECT zip, primary_city, 
       latitude, longitude, distance_in_mi
  FROM (
SELECT zip, primary_city, latitude, longitude,r,
       (3963.17 * ACOS(COS(RADIANS(latpoint)) 
                 * COS(RADIANS(latitude)) 
                 * COS(RADIANS(longpoint) - RADIANS(longitude)) 
                 + SIN(RADIANS(latpoint)) 
                 * SIN(RADIANS(latitude)))) AS distance_in_mi
 FROM zip
 JOIN (
        SELECT  42.81  AS latpoint,  -70.81 AS longpoint, 50.0 AS r
   ) AS p 
 WHERE latitude  
  BETWEEN latpoint  - (r / 69) 
      AND latpoint  + (r / 69)
   AND longitude 
  BETWEEN longpoint - (r / (69 * COS(RADIANS(latpoint))))
      AND longpoint + (r / (69 * COS(RADIANS(latpoint))))
  ) d
 WHERE distance_in_mi <= r
 ORDER BY distance_in_mi
 LIMIT 30

Regardez cette ligne au milieu de cette requête:

    SELECT  42.81  AS latpoint,  -70.81 AS longpoint, 50.0 AS r

Ceci recherche les 30 entrées les plus proches dans le ziptableau à moins de 50,0 miles du point lat / long 42,81 / -70,81. Lorsque vous intégrez cela dans une application, c'est là que vous mettez votre propre point et rayon de recherche.

Si vous souhaitez travailler en kilomètres plutôt qu'en miles, passez 69à 111.045et changez 3963.17en 6378.10dans la requête.

Voici un résumé détaillé. J'espère que cela aide quelqu'un. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
la source
3

J'ai écrit une procédure qui peut calculer la même chose, mais vous devez entrer la latitude et la longitude dans le tableau respectif.

drop procedure if exists select_lattitude_longitude;

delimiter //

create procedure select_lattitude_longitude(In CityName1 varchar(20) , In CityName2 varchar(20))

begin

    declare origin_lat float(10,2);
    declare origin_long float(10,2);

    declare dest_lat float(10,2);
    declare dest_long float(10,2);

    if CityName1  Not In (select Name from City_lat_lon) OR CityName2  Not In (select Name from City_lat_lon) then 

        select 'The Name Not Exist or Not Valid Please Check the Names given by you' as Message;

    else

        select lattitude into  origin_lat from City_lat_lon where Name=CityName1;

        select longitude into  origin_long  from City_lat_lon where Name=CityName1;

        select lattitude into  dest_lat from City_lat_lon where Name=CityName2;

        select longitude into  dest_long  from City_lat_lon where Name=CityName2;

        select origin_lat as CityName1_lattitude,
               origin_long as CityName1_longitude,
               dest_lat as CityName2_lattitude,
               dest_long as CityName2_longitude;

        SELECT 3956 * 2 * ASIN(SQRT( POWER(SIN((origin_lat - dest_lat) * pi()/180 / 2), 2) + COS(origin_lat * pi()/180) * COS(dest_lat * pi()/180) * POWER(SIN((origin_long-dest_long) * pi()/180 / 2), 2) )) * 1.609344 as Distance_In_Kms ;

    end if;

end ;

//

delimiter ;
Abdul Manaf
la source
3

Je ne peux pas commenter la réponse ci-dessus, mais soyez prudent avec la réponse de @Pavel Chuchuva. Cette formule ne retournera pas de résultat si les deux coordonnées sont identiques. Dans ce cas, la distance est nulle et cette ligne ne sera donc pas renvoyée avec cette formule telle quelle.

Je ne suis pas un expert MySQL, mais cela semble fonctionner pour moi:

SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance 
FROM markers HAVING distance < 25 OR distance IS NULL ORDER BY distance LIMIT 0 , 20;
John Crenshaw
la source
2
Si les positions sont identiques, il ne devrait pas sortir NULL, mais égal à zéro (comme ACOS(1)0). Vous pourriez voir des problèmes d'arrondi avec les xaxis * xaxis + yaxis * yaxis + zaxis * zaxis hors de portée pour ACOS, mais vous ne semblez pas vous prémunir contre cela?
Rowland Shaw
3
 SELECT *, (  
    6371 * acos(cos(radians(search_lat)) * cos(radians(lat) ) *   
cos(radians(lng) - radians(search_lng)) + sin(radians(search_lat)) *         sin(radians(lat)))  
) AS distance  
FROM table  
WHERE lat != search_lat AND lng != search_lng AND distance < 25  
 ORDER BY distance  
FETCH 10 ONLY 

pour une distance de 25 km

Harish Lalwani
la source
Le dernier (radians (lat) doit être sin (radians (lat))
KGs
im obtenant une erreur "distance de colonne inconnue" pourquoi est-ce?
Jill John le
@JillJohn si vous ne voulez que la distance, vous pouvez supprimer complètement l'ordre par distance. Si vous voulez trier les résultats, vous pouvez utiliser ceci - ORDER BY (6371 * acos (cos (radians (search_lat)) * cos (radians (lat)) * cos (radians (lng) - radians (search_lng)) + sin (radians (search_lat)) * sin (radians (lat)))).
Harish Lalwani
2

Je pensais que mon implémentation javascript serait une bonne référence à:

/*
 * Check to see if the second coord is within the precision ( meters )
 * of the first coord and return accordingly
 */
function checkWithinBound(coord_one, coord_two, precision) {
    var distance = 3959000 * Math.acos( 
        Math.cos( degree_to_radian( coord_two.lat ) ) * 
        Math.cos( degree_to_radian( coord_one.lat ) ) * 
        Math.cos( 
            degree_to_radian( coord_one.lng ) - degree_to_radian( coord_two.lng ) 
        ) +
        Math.sin( degree_to_radian( coord_two.lat ) ) * 
        Math.sin( degree_to_radian( coord_one.lat ) ) 
    );
    return distance <= precision;
}

/**
 * Get radian from given degree
 */
function degree_to_radian(degree) {
    return degree * (Math.PI / 180);
}
Sam Vloeberghs
la source
0

calculer la distance en Mysql

 SELECT (6371 * acos(cos(radians(lat2)) * cos(radians(lat1) ) * cos(radians(long1) -radians(long2)) + sin(radians(lat2)) * sin(radians(lat1)))) AS distance

ainsi la valeur de distance sera calculée et n'importe qui peut postuler si nécessaire.

Rajesh Prasad Yadav
la source