Saltar al contenido

Distancia del gran círculo de MySQL (fórmula de Haversine)

Solución:

De las preguntas frecuentes de Google Code: creación de un localizador de tiendas con PHP, MySQL y Google Maps:

Aquí está la declaración SQL que encontrará las 20 ubicaciones más cercanas que se encuentran dentro de un radio de 25 millas a la coordenada 37, -122. Calcula la distancia en función de la latitud / longitud de esa fila y la latitud / longitud objetivo, y luego solicita solo las filas donde el valor de la distancia es menor que 25, ordena toda la consulta por distancia y la limita a 20 resultados. Para buscar por kilómetros en lugar de millas, reemplace 3959 con 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;

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

con latitud y longitud en radianes.

asi que

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

es tu consulta SQL

para obtener sus resultados en Km o millas, multiplique el resultado con el radio medio de la Tierra (3959 millas,6371 Km o 3440 millas náuticas)

Lo que está calculando en su ejemplo es un cuadro delimitador. Si coloca sus datos de coordenadas en una columna de MySQL habilitada espacialmente, puede usar la funcionalidad incorporada de MySQL para consultar los datos.

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

Si agrega campos auxiliares a la tabla de coordenadas, puede mejorar el tiempo de respuesta de la consulta.

Como esto:

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 está utilizando TokuDB, obtendrá un rendimiento aún mejor si agrega índices de agrupamiento en cualquiera de los predicados, por ejemplo, como este:

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

Necesitará la latitud y la longitud básicas en grados, así como sin (lat) en radianes, cos (lat) * cos (lon) en radianes y cos (lat) * sin (lon) en radianes para cada punto. Luego crea una función mysql, algo como esto:

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

Esto te da la distancia.

No olvide agregar un índice en lat / lon para que el cuadro delimitador pueda ayudar a la búsqueda en lugar de ralentizarla (el índice ya está agregado en la consulta CREATE TABLE anterior).

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

Dada una tabla antigua con solo coordenadas lat / lon, puede configurar un script para actualizarlo así: (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']
  ));
}

Luego, optimiza la consulta real para que solo haga el cálculo de la distancia cuando sea realmente necesario, por ejemplo, delimitando el círculo (bueno, ovalado) desde adentro y desde afuera. Para eso, necesitará calcular previamente varias métricas para la consulta en sí:

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

Dados esos preparativos, la consulta es algo como esto (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);

EXPLICAR en la consulta anterior podría decir que no está usando el índice a menos que haya suficientes resultados para activarlo. El índice se utilizará cuando haya suficientes datos en la tabla de coordenadas. Puede agregar FORCE INDEX (lat_lon_idx) al SELECT para que use el índice sin tener en cuenta el tamaño de la tabla, para que pueda verificar con EXPLAIN que está funcionando correctamente.

Con los ejemplos de código anteriores, debería tener una implementación funcional y escalable de búsqueda de objetos por distancia con un error mínimo.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *