Distância Grande Círculo do MySQL (fórmula Haversine)

184

Eu tenho um script PHP que obtém os valores de Longitude e Latitude e os insere em uma consulta MySQL. Eu gostaria de torná-lo apenas MySQL. Aqui está o meu código PHP atual:

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

Alguém sabe como fazer isso inteiramente MySQL? Eu naveguei um pouco na Internet, mas a maioria da literatura é bastante confusa.

Nick Woodhams
fonte
4
Com base em todas as excelentes respostas abaixo, exemplo aqui está trabalhando da fórmula Haversine em ação
StartupGuy
Obrigado por compartilhar que Michael.M
Nick Woodhams
stackoverflow.com/a/40272394/1281385 tem um exemplo de como se certificar de índice são atingidos
exussum

Respostas:

357

Do FAQ do Google Code - Criando um localizador de lojas com PHP, MySQL e Google Maps :

Aqui está a instrução SQL que encontrará os 20 locais mais próximos que estão dentro de um raio de 40 km até a coordenada 37, -122. Ele calcula a distância com base na latitude / longitude dessa linha e na latitude / longitude alvo e, em seguida, solicita apenas linhas onde o valor da distância é menor que 25, ordena toda a consulta por distância e limita a 20 resultados. Para pesquisar por quilômetros em vez de milhas, substitua 3959 por 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
fonte
2
a instrução sql é realmente boa. mas onde posso passar minhas coordenadas para esta declaração? eu não posso ver coordenadas em qualquer lugar se passaram
Mann
32
Substitua 37 e -122 pelas suas coordenadas.
Pavel Chuchuva
5
Eu me pergunto sobre as implicações desse desempenho se existem milhões de lugares (+ milhares de visitantes) ...
Halil Özgür
12
Você pode restringir a consulta para um melhor desempenho, conforme explicado nesta doc: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas
2
@FosAvance Sim, esta consulta funcionaria se você tiver markerstabela com os campos id, lan e lng.
Pavel Chuchuva 29/03
32

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

com latitude e longitude em radiano.

tão

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

é sua consulta SQL

para obter seus resultados em km ou milhas, multiplique o resultado pelo raio médio da Terra ( 3959milhas, 6371km ou 3440milhas náuticas)

O que você está calculando no seu exemplo é uma caixa delimitadora. Se você colocar seus dados de coordenadas em uma coluna do MySQL espacial , poderá usar a funcionalidade de compilação do MySQL para consultar os dados.

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

Se você adicionar campos auxiliares à tabela de coordenadas, poderá melhorar o tempo de resposta da consulta.

Como isso:

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`)
)    

Se você estiver usando o TokuDB, obterá um desempenho ainda melhor se adicionar índices de cluster em qualquer um dos predicados, por exemplo, assim:

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

Você precisará do lat e lon básico em graus, bem como do pecado (lat) em radianos, cos (lat) * cos (lon) em radianos e cos (lat) * sin (lon) em radianos para cada ponto. Então você cria uma função mysql, algo assim:

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

Isso lhe dá a distância.

Não se esqueça de adicionar um índice em lat / lon, para que o boxe delimitador possa ajudar na pesquisa, em vez de diminuí-la (o índice já foi adicionado na consulta CREATE TABLE acima).

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

Dada uma tabela antiga com apenas coordenadas lat / lon, você pode configurar um script para atualizá-lo assim: (php usando 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']
  ));
}

Em seguida, você otimiza a consulta real para fazer o cálculo da distância apenas quando realmente necessário, por exemplo, delimitando o círculo (bem, oval) por dentro e por fora. Para isso, você precisará pré-calcular várias métricas para a própria consulta:

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

Dadas essas preparações, a consulta é mais ou menos assim (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 na consulta acima pode dizer que não está usando índice, a menos que haja resultados suficientes para acioná-lo. O índice será usado quando houver dados suficientes na tabela de coordenadas. Você pode adicionar o FORCE INDEX (lat_lon_idx) ao SELECT para fazer com que ele use o índice sem considerar o tamanho da tabela, para verificar com EXPLAIN se está funcionando corretamente.

Com os exemplos de código acima, você deve ter uma implementação funcional e escalável da pesquisa de objetos à distância com um erro mínimo.

silvio
fonte
10

Eu tive que resolver isso com mais detalhes, então vou compartilhar meu resultado. Isso usa uma ziptabela com latitudee longitudetabelas. Não depende do Google Maps; em vez disso, você pode adaptá-lo a qualquer tabela que contenha 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

Veja esta linha no meio dessa consulta:

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

zipEle procura as 30 entradas mais próximas na tabela a 50,0 milhas do ponto lat / long 42,81 / -70,81. Quando você cria isso em um aplicativo, é aí que você coloca seu próprio ponto e raio de pesquisa.

Se você deseja trabalhar em quilômetros, e não em milhas, mude 69para 111.045e mude 3963.17para 6378.10na consulta.

Aqui está um artigo detalhado. Espero que ajude alguém. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
fonte
3

Eu escrevi um procedimento que pode calcular o mesmo, mas você deve inserir a latitude e longitude na respectiva tabela.

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
fonte
3

Não posso comentar a resposta acima, mas tenha cuidado com a resposta de @Pavel Chuchuva. Essa fórmula não retornará um resultado se as duas coordenadas forem iguais. Nesse caso, a distância é nula e, portanto, a linha não será retornada com a fórmula como está.

Eu não sou um especialista em MySQL, mas isso parece estar funcionando para mim:

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
fonte
2
Se as posições forem idênticas, não deve sair NULL, mas sim zero (como ACOS(1)é 0). Você pode encontrar problemas de arredondamento com os zaxis xaxis * xaxis + yaxis * yaxis + zaxis * fora do intervalo para o ACOS, mas você não parece se proteger disso?
precisa
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 

para distância de 25 km

Harish Lalwani
fonte
O último (radianos (lat) deve ser pecado (radianos (lat)))
KGs 8/17
estou recebendo um erro "distância desconhecida da coluna" por que isso?
Jill John
@JillJohn se você quiser apenas distância, poderá remover completamente o pedido por distância. Se você deseja classificar os resultados, pode usar isso - ORDER BY (6371 * acos (cos (radianos (search_lat)) * cos (radianos (lat)) * cos (radianos (lng) - radianos (search_lng)) + sin (radianos (search_lat)) * sin (radianos (lat)))).
Harish Lalwani 11/04
2

Eu pensei que minha implementação javascript seria uma boa referência para:

/*
 * 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
fonte
0

calcular distância no Mysql

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

assim, o valor da distância será calculado e qualquer pessoa poderá aplicar conforme necessário.

Rajesh Prasad Yadav
fonte