2012-01-04 13 views
7

OK - He estado luchando con esto durante aproximadamente 3 meses de forma intermitente y desde que he agotado todas las fórmulas de proximidad geográfica que he encontrado y que No estoy más cerca de obtener los resultados correctos, pensé que era hora de pedir ayuda.Faltan resultados debido a la fórmula de proximidad geográfica (localizador de tiendas)

EL OBJETIVO

Soy la creación de una aplicación bastante básica de un localizador de tiendas. El usuario ingresa su código postal y selecciona de una lista predefinida de radios de búsqueda. La API de gmaps genera coordenadas lat/long para esta dirección y las pasa a una secuencia de comandos php. En este script las coordenadas de usuario se preguntó en contra de una tabla de base de datos MySQL (estructura de abajo)

post_id int(11)        
post_type varchar(20)         
lat float(10,6)        
lng float(10,6) 

Los resultados de esta consulta (post IDS) se introducen en una consulta de wordpress que genera el XML que contiene los datos del mapa de marcadores. (La consulta wordpress utiliza post__in y posts_per_page -1 para mostrar la información para todos ID generado por la consulta

EL PROBLEMA

En pocas palabras, cada implementación de la fórmula Haversine que he encontrado parece ser el resultado en marcadores faltantes, específicamente cualquier marcador que esté muy cerca de las coordenadas ingresadas por los usuarios (no lo sé con precisión, pero creo que está dentro de los 500 m). Este es un gran problema, como si el usuario ingresara su código postal y hay una tienda muy cerca a su ubicación no aparecerá.

He intentado unas 8 permutaciones diferentes de la forumla que he desenterrado fr om varios tutoriales con los mismos resultados. A continuación se muestra la fórmula que estoy usando actualmente en el sitio que ofrece todos los marcadores excepto por el los más cercanos a los usuarios la posición entrado:

$center_lat = $_GET["lat"]; 
$center_lng = $_GET["lng"]; 
$radius = $_GET["radius"]; 

// Calculate square radius search 

$lat1 = (float) $center_lat - ((int) $radius/69); 
$lat2 = (float) $center_lat + ((int) $radius/69); 
$lng1 = (float) $center_lng - (int) $radius/abs(cos(deg2rad((float) $center_lat)) * 69); 
$lng2 = (float) $center_lng + (int) $radius/abs(cos(deg2rad((float) $center_lat)) * 69); 

$sqlsquareradius = " 
SELECT 
post_id, lat, lng 
FROM 
wp_geodatastore 
WHERE 
lat BETWEEN ".$lat1." AND ".$lat2." 
AND 
lng BETWEEN ".$lng1." AND ".$lng2." 
"; // End $sqlsquareradius 

// Create sql for circle radius check 
$sqlcircleradius = " 
SELECT 
t.post_id, 
3956 * 2 * ASIN(
    SQRT(
     POWER(
      SIN(
       (".(float) $center_lat." - abs(t.lat)) * pi()/180/2 
      ), 2 
     ) + COS(
      ".(float) $center_lat." * pi()/180 
     ) * COS(
      abs(t.lat) * pi()/180 
     ) * POWER(
      SIN(
       (".(float) $center_lng." - t.lng) * pi()/180/2 
      ), 2 
     ) 
    ) 
) AS distance 
FROM 
(".$sqlsquareradius.") AS t 
HAVING 
distance <= ".(int) $radius." 
ORDER BY distance 
"; // End $sqlcircleradius 


$result = mysql_query($sqlcircleradius); 

$row = mysql_fetch_array($result); 

while($row = mysql_fetch_array($result)) { 
// the contents of each row 
$post_ids[] = $row['post_id']; 
} 

Hubo 1 fórmula que he intentado que se sugirió por Mike Pelley aquí : Geolocation SQL query not finding exact location

Esta fórmula parecía mostrar marcadores que estaban muy cerca de la ubicación de los usuarios introducidos, pero se perdieron otros que deberían haberse mostrado dentro del radio dado. Para aclarar cualquier confusión que esto es el código que utilicé:

$center_lat = $_GET["lat"]; 
$center_lng = $_GET["lng"]; 
$radius = $_GET["radius"]; 

$sql = " 
SELECT post_id, lat, lng, 
truncate((degrees(acos(sin(radians(lat)) 
* sin(radians(".$center_lat.")) 
+ cos(radians(lat)) 
* cos(radians(".$center_lat.")) 
* cos(radians(".$center_lng." - lng)))) 
* 69.09*1.6),1) as distance 
FROM wp_geodatastore HAVING distance <= ".$radius." ORDER BY distance desc 
"; // End $sqlcircleradius 


$result = mysql_query($sql); 

$row = mysql_fetch_array($result); 

while($row = mysql_fetch_array($result)) { 
// Print out the contents of each row 
$post_ids[] = $row['post_id']; 
} 

LA SOLICITUD

Básicamente me gustaría saber por qué ninguno de estos bloques de código están mostrando los marcadores correctos. Si alguien puede sugerir una mejora al código o me puede apuntar hacia algún recurso que podría haber perdido que sería grande

EDITAR

pensaba que mi respuesta psudeo funcionaba pero como resulta que todavía estaba teniendo problemas. Terminé yendo por un rumbo muy diferente ahora y estoy usando un muy buen localizador de tiendas jquery que se puede encontrar aquí: http://www.bjornblog.com/web/jquery-store-locator-plugin

No funcionará para todos los proyectos, pero para mis necesidades es perfecto (y funciona!)

+0

¿Hay alguna razón por la que no esté utilizando las capacidades geospaciales integradas de MySQL? http://dev.mysql.com/doc/refman/5.0/en/creating-a-spatially-enabled-mysql-database.html – Kenneth

+0

Estoy luchando por comprender algunas cosas en tu código. ¿Por qué 'HAVING' en lugar de' WHERE'? ¿Qué está pasando con las cosas 'int' en' (float) $ center_lat - ((int) $ radius/69); 'y' truncado' en su otra consulta? Tenga en cuenta este hecho al aplicar las fórmulas: un minuto de longitud en el ecuador se define como una milla náutica. Un grado entero es 60 millas náuticas. Finalmente, intente deshacerse de 'BETWEEN' y usar 'WHERE a> = lat1 AND a <= lat2' en su lugar. Debe dar la misma complejidad de consulta y explica la inclusividad/exclusividad del rango que está buscando. –

+0

@Kenneth - No estoy utilizando las consultas geoespaciales por un par de razones. Primero, estoy usando el [plugin de la tienda de geodatos] (http://wordpress.org/extend/plugins/geo-data-store) para crear y mantener mi tabla de datos de marcadores. Este complemento genera la estructura de tabla que mostré arriba. En segundo lugar, la gran mayoría de los tutoriales que he encontrado para crear mapas de tipo localizador de tiendas parecen recomendar una estructura de tabla similar a la que figura en la lista. Tal vez estas no sean las mejores razones, pero he llegado hasta aquí con la configuración actual y estoy bastante seguro de que lo que estoy tratando de hacer debería ser posible. – FourStacks

Respuesta

0

Pensando un poco lateralmente, he encontrado una "especie de" solución al problema de los marcadores faltantes. Las dos ecuaciones que publiqué originalmente dieron los resultados correctos pero cada una omitió marcadores cerca del objetivo o en los bordes del radio de búsqueda

No es muy elegante pero pensé que ejecutando ambas ecuaciones y produciendo 2 arreglos que luego combinado (eliminar cualquier duplicado) me daría todos los marcadores que estoy buscando. Esto funciona (obviamente, un éxito de rendimiento, pero no es una aplicación de alto tráfico), así que voy a trabajar con esto por el momento, pero todavía estoy buscando una solución más práctica si alguien tiene una.

0

Aquí hay una solución He utilizado con éxito por un tiempo en mis propios cálculos geográfica de proximidad:

/** 
* This portion of the routine calculates the minimum and maximum lat and 
* long within a given range. This portion of the code was written 
* by Jeff Bearer (http:return true;//www.jeffbearer.com). 
*/ 

$lat = somevalue;  // The latitude of our search origin 
$lon = someothervalue; // The longitude of our search origin 
$range = 50; // The range of our search, in miles, of your zip 

// Find Max - Min Lat/Long for Radius and zero point and query only zips in that range. 
$lat_range = $range/69.172; 
$lon_range = abs($range/(cos($lon) * 69.172)); 
$min_lat = number_format($lat - $lat_range, '4', '.', ''); 
$max_lat = number_format($lat + $lat_range, '4', '.', ''); 
$min_lon = number_format($lon - $lon_range, '4', '.', ''); 
$max_lon = number_format($lon + $lon_range, '4', '.', ''); 

/* Query for matching zips: 

    SELECT post_id, lat, lng 
    FROM wp_geodatastore 
    WHERE 
    lat BETWEEN $min_lat AND $max_lat 
    AND lng BETWEEN $min_lon AND $max_lon 
*/ 
+0

Gracias por publicar el código @Fleep. Lamentablemente, aunque funcionó en general, todavía terminé con el mismo problema de marcador faltante. ¡Aprecia el intento! – FourStacks

2

EDITAR Esta ubicación del buscador se acerca bastante a menudo que he escrito un artículo sobre él.

http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

Post original

Vamos a empezar por tratar con la fórmula haversine una vez por todas, poniéndolo en una función almacenada por lo que podemos olvidarnos de sus detalles retorcidos. NOTA: toda esta solución está en millas estatutarias.

DELIMITER $$ 

CREATE 
    FUNCTION distance(lat1 FLOAT, long1 FLOAT, lat2 FLOAT, long2 FLOAT) 
    RETURNS FLOAT 
    DETERMINISTIC NO SQL 
    BEGIN 
    RETURN (3959 * ACOS(COS(RADIANS(lat1)) 
       * COS(RADIANS(lat2)) 
       * COS(RADIANS(long1) - RADIANS(long2)) 
       + SIN(RADIANS(lat1)) 
       * SIN(RADIANS(lat2)) 
       )); 
    END$$ 

DELIMITER ; 

Ahora vamos a poner juntos una consulta que busca en el cuadro delimitador y refina la búsqueda con nuestra función de distancia y pedidos por distancia

Basado en el código PHP en la pregunta:

Supongamos $radius es su radio, $center_lat, $center_lng es su punto de referencia.

$sqlsquareradius = " 
SELECT post_id, lat, lng 
    FROM 
(
    SELECT post_id, lat, lng, 
      distance(lat, lng, " . $center_lat . "," . $center_lng . ") AS distance 
     FROM wp_geodatastore 
    WHERE lat >= " . $center_lat . " -(" . $radius . "/69) 
     AND lat <= " . $center_lat . " +(" . $radius . "/69) 
     AND lng >= " . $center_lng . " -(" . $radius . "/69) 
     AND lng <= " . $center_lng . " +(" . $radius . "/69) 
)a 
WHERE distance <= " . $radius . " 
ORDER BY distance 
"; 

Tenga en cuenta algunas cosas sobre esto.

En primer lugar, hace el cálculo del cuadro delimitador en SQL en lugar de hacerlo en PHP. No hay una buena razón para eso, excepto mantener todos los cálculos en un solo entorno. (radius/69) es el número de grados en radius millas terrestres.

En segundo lugar, no juega con el tamaño del cuadro delimitador longitudinal basado en la latitud. En su lugar, utiliza un cuadro delimitador más simple, pero un poco demasiado grande. Este recuadro delimita algunos registros extra, pero la medición de distancia se deshace de ellos. Para su aplicación típica de código postal/buscador de tienda, la diferencia de rendimiento es insignificante. Si estuviera buscando muchos más registros (por ejemplo, una base de datos de todos los polos de servicios públicos), podría no ser tan trivial.

En tercer lugar, utiliza una consulta anidada para eliminar la distancia, para evitar tener que ejecutar la función de distancia más de una vez para cada elemento.

En cuarto lugar, ordena por distancia ASCENDING. Esto significa que los resultados de distancia cero deben aparecer primero en el conjunto de resultados. Por lo general, tiene sentido enumerar las cosas más cercanas primero.

En quinto lugar, utiliza FLOAT en lugar de DOUBLE en todo. Hay una buena razón para eso. La fórmula de distancia haversine no es perfecta, porque hace la aproximación de que la tierra es una esfera perfecta. Esa aproximación se rompe a aproximadamente el mismo nivel de precisión que el épsilon para los números FLOAT. Entonces, DOUBLE es una superación numérica engañosa para este problema. (No use esta fórmula de haversine para hacer trabajos de ingeniería civil, como drenaje de estacionamiento, o obtendrá grandes charcos un par de épsilon, unos centímetros, profundo, lo prometo). Está bien para aplicaciones de buscador de tienda.

En sexto lugar, definitivamente querrá crear un índice para su columna lat. Si su tabla de ubicaciones no cambia muy a menudo, le ayudará a crear un índice para su columna lng también.Pero su índice lat le dará la mayor parte de la ganancia de rendimiento de su consulta.

Por último, probé el procedimiento almacenado y el SQL, pero no el PHP.

Referencia: http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL También mi experiencia con un grupo de buscadores de proximidad para centros de salud.

--------------- EDITAR --------------------

Si usted no tiene una interfaz de usuario que le permite definir un procedimiento almacenado, eso es una molestia. En cualquier caso, PHP le permite usar parámetros numerados en la llamada sprintf, para que pueda generar toda la declaración anidada de esta manera. NOTA: es posible que necesite% $ 1f, etc. Tendrá que experimentar con esto.

$sql_stmt = sprintf (" 
    SELECT post_id, lat, lng 
    FROM 
    (
    SELECT post_id, lat, lng, 
      (3959 * ACOS(COS(RADIANS(lat)) 
       * COS(RADIANS(%$1s)) 
       * COS(RADIANS(lng) - RADIANS(%$2s)) 
       + SIN(RADIANS(lat)) 
       * SIN(RADIANS(%$1s)) 
      )) 
      AS distance 
     FROM wp_geodatastore 
    WHERE lat >= %$1s -(%$3s/69) 
     AND lat <= %$1s +(%$3s/69) 
     AND lng >= %$2s -(%$3s/69) 
     AND lng <= %$2s +(%$3s/69) 
)a 
    WHERE distance <= %$3s 
    ORDER BY distance 
",$center_lat,$center_lng, $radius); 
+0

Gracias por la respuesta muy detallada Ollie. Sin embargo, tuvo algunos problemas para implementarlo para probarlo. Lamento ser un dolor pero realmente no estoy familiarizado con los procedimientos almacenados de MySQL (puedo abrirme camino en una base de datos MySQL con PhpMyAdmin, pero ahí es donde se agota mi conocimiento de MySQL). Busqué algunos tutoriales sobre la creación de funciones almacenadas utilizando esta GUI, pero no encontré nada que me guíe. ¿Es vital que la fórmula de haversine esté en este formato o podría incluirse con el resto de la consulta php? – FourStacks

+0

Gracias por la edición de Ollie, gracias por convertirlo a php y toda la ayuda hasta el momento. Lamentablemente, no parece producir ningún valor de ID en absoluto. Double duplicó el código para asegurarse de que no hice nada estúpido, no pude ver nada. También probé su sugerencia de cambiar el especificador de tipo en el sprintf, pero sin cambios. No estaba seguro de si la minúscula 'a' en su código era intencional o un error tipográfico, pero de cualquier manera no produjo ningún resultado. – FourStacks

+0

Atribuí la recompensa a esta respuesta porque fue la respuesta más completa y completamente explicada (a pesar de que en realidad no la utilicé al final, vea mi propia respuesta a continuación, que estoy rodando por ahora). Dicho esto, esta pregunta generó algunas respuestas muy buenas y estoy seguro de que las ecuaciones que se enumeran a continuación probablemente funcionen para proyectos de otras personas con una configuración diferente a la mía, por lo que merecen una visita. – FourStacks

0

Puedes probar mi clase en http://www.phpclasses.org/package/6202-PHP-Generate-points-of-an-Hilbert-curve.html. Utiliza la fórmula harvesine y una curva hilbert para calcular un quadkey. A continuación, puede buscar el quadkey de izquierda a derecha. Cada posición de la tecla es un punto en la curva del monstruo. Se puede encontrar una mejor explicación de la curva en el blog de la curva de hilbert del quadtree de índice espacial de Nick. Es como usar la extensión de índice espacial de mysql pero tienes más control. Puede usar una curva z o una curva moore o puede cambiar el aspecto.

0

Este es el código de un sistema de producción de trabajo,

6371.04 * acos(cos(pi()/2-radians(90-wgs84_lat)) * cos(pi()/2-radians(90-$lat)) * cos(radians(wgs84_long)-radians($lon)) + sin(pi()/2-radians(90-wgs84_lat)) * sin(pi()/2-radians(90-$lat))) as distance 

Utiliza un formular distancia diferente, pero para un localizador de tiendas la diferencia es mínima.

Cuestiones relacionadas