Pesquisas geográficas a uma certa distância

Cenário

Esta postagem se origina no meu blog pessoal, em http://www.mullie.eu/geographic-searches/

Uma localização bidimensional em nossa Terra pode ser representada por meio de um sistema de coordenadas semelhante a um eixo X e Y. Esses eixos são chamados de latitude (lat) e longitude (lng).

Latitude é o eixo norte-sul com um mínimo de -90 (pólo sul) e máximo de 90 graus (pólo norte). O equador está a zero graus de latitude.

A longitude é o equivalente ao eixo X, percorrendo o globo de leste a oeste: de -180 a +180 graus. O meridiano de Greenwich tem 0 graus de longitude. Tudo a oeste e leste dela é respectivamente negativo e positivo na escala de longitude, até o meio do Oceano Pacífico, perto da Linha Internacional de Data , onde -180 ° de longitude se cruza para 180 °.

Atualização : criei um pequeno repositório com todo o código abaixo em algumas pequenas classes organizadas. Se você está procurando calcular a distância entre várias coordenadas ou calcular uma caixa delimitadora para encontrar as coordenadas próximas em seu banco de dados, pode fazer sentido dar uma olhada .

Coordenadas e quilômetros / milhas

Sempre disseram a você que a Groenlândia não é tão grande quanto é representada em um mapa 2D médio. É mais ou menos do tamanho do Congo, mas em uma projeção 2D , as bordas da Terra parecem maiores do que realmente são.

Como a Terra é (quase) esférica em vez de bidimensional, a distância entre 2 coordenadas não é linear. A distância entre 0 e 10 graus de longitude, na realidade, é muito menor no pólo norte do que no equador.

Para calcular a distância panorâmica entre 2 coordenadas, a geometria finalmente é útil!

O raio da Terra é de aproximadamente 6.371 quilômetros ou 3959 milhas. O referido raio multiplicado pela distância do grande círculo calculada entre as 2 coordenadas mapeadas em uma esfera, deve render a distância entre os dois pontos.

function distance($lat1, $lng1, $lat2, $lng2) {
// convert latitude/longitude degrees for both coordinates
// to radians: radian = degree * π / 180
$lat1
= deg2rad($lat1);
$lng1
= deg2rad($lng1);
$lat2
= deg2rad($lat2);
$lng2
= deg2rad($lng2);

// calculate great-circle distance
$distance
= acos(sin($lat1) * sin($lat2) + cos($lat1) * cos($lat2) * cos($lng1 - $lng2));

// distance in human-readable format:
// earth's radius in km = ~6371
return 6371 * $distance;
}

Observe que a Terra não é exatamente esférica: o raio da Terra é ligeiramente maior no equador (~ 6378 km) do que nos pólos (~ 6356 km), então a distância exata que acabamos de calcular pode estar ligeiramente diferente.

Se, em vez da distância panorâmica, você está procurando calcular a distância de viagem na estrada entre 2 pontos, provavelmente é melhor usar a API da matriz de distância do Google .

Encontre locais próximos no banco de dados

Embora existam soluções muito superiores (como ElasticSearch ) para realizar pesquisas geográficas, você pode encontrar seus dados presos em um banco de dados relacional, como o MySQL . O MySQL também tem uma extensão SPATIAL para facilitar as operações baseadas em geografia (embora eu não o use muito, na verdade acho mais fácil lidar com os dados brutos sozinho.)

Uma pesquisa comum baseada em localização e distância é “encontre tudo em um raio de X quilômetros”. Existem várias maneiras de fazer isso. Você poderia, por exemplo, criar uma condição WHERE que imita a fórmula acima mencionada com base na distância do grande círculo para calcular a diferença entre todas as coordenadas em seu banco de dados e o ponto dado, para deixar de fora todas as entradas onde a distância é maior do que você ‘ d gosto. Depois que seu banco de dados ficar muito grande, você não quer realmente calcular a distância para cada local em seu banco de dados: levará algum tempo para calcular todas essas diferenças e não há como usar um índice.

Em vez disso, queremos encontrar um subconjunto aproximado de resultados dentro de certos limites fixos. Esses limites são os valores máximos e mínimos de latitude e longitude de sua coordenada mais / menos a distância. Podemos calculá-los como:

// we'll want everything within, say, 10km distance
$distance
= 10;

// earth's radius in km = ~6371
$radius
= 6371;

// latitude boundaries
$maxlat
= $lat + rad2deg($distance / $radius);
$minlat
= $lat - rad2deg($distance / $radius);

// longitude boundaries (longitude gets smaller when latitude increases)
$maxlng
= $lng + rad2deg($distance / $radius / cos(deg2rad($lat)));
$minlng
= $lng - rad2deg($distance / $radius / cos(deg2rad($lat)));

Agora que temos esses limites externos, podemos buscar resultados em nosso banco de dados como este (observe como um índice agora pode ser usado para recuperar valores correspondentes para latitude / longitude):

SELECT *
FROM coordinates

WHERE

lat BETWEEN
:minlat AND :maxlat
lng BETWEEN
:minlng AND :maxlng

Ou usando a extensão espacial (nenhum ponto em manutenção late lngcarros alegóricos aqui; coordinateé um Point: ):GeomFromText(CONCAT("Point(", :lat, " ", :lng, ")"))

WHERE MBRWithin(coordinate, GeomFromText(CONCAT("Polygon((", :maxlat, " ", :maxlng, ",", :maxlat, " ", :minlng, ",", :minlat, " ", :minlng, ",", :minlat, " ", :maxlng, ",", :maxlat, " ", :maxlng, "))")))

Nós maximizamos a recuperação rápida de coordenadas, mas nem todas as coordenadas coincidentes estão realmente dentro da distância que desejamos coincidir. Usando esses limites, consultamos uma área semelhante a um quadrado 2D, mas na verdade queremos encontrar resultados em uma área semelhante a um círculo. Aqui está uma imagem para simplificar por que ainda não terminamos:

Cenário

A caixa preta significa a área que acabamos de consultar no banco de dados. O círculo laranja representa o que na verdade seria um limite real de 10 quilômetros. Observe como ambas as coordenadas brancas estão dentro dos limites aproximados, mas apenas a inferior está realmente dentro da distância solicitada.

Para eliminar esses resultados que caíram em nossos limites aproximados, mas não estão realmente dentro da distância da área desejada, vamos apenas fazer um loop em todas essas entradas e calcular a distância lá. Como nosso conjunto de resultados agora será muito pequeno, isso não deve nos prejudicar:

// our own location & distance we want to search
$lat
= 50.52;
$lng
= 4.22;
$distance
= 10;

// weed out all results that turn out to be too far
foreach ($results as $i => $result) {
$resultDistance
= distance($lat, $lng, $result['lat'], $result['lng']);
if ($resultDistance > $distance) {
unset
($results[$i]);
}
}

Tadaa! Todas as coordenadas dentro de uma determinada distância!