Отсутствуют результаты из-за формулы географической близости (поиск магазина) - PullRequest
7 голосов
/ 04 января 2012

ОК - я боролся с этим около 3 месяцев и с тех пор, как исчерпал все формулы гео-близости, с которыми я сталкивался, и я не приблизился к получению правильных результатов, как я полагал.пришло время обратиться за помощью.

ЦЕЛЬ

Я настраиваю довольно простую реализацию локатора магазина.Пользователь вводит свой почтовый индекс и выбирает из предварительно определенного списка радиусов поиска.API gmaps генерирует координаты широты и долготы для этого адреса и передает их в скрипт php.В этом сценарии пользовательские координаты запрашиваются в таблице базы данных mysql (структура ниже)

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

Результаты этого запроса (идентификаторы записей) вводятся в запрос wordpress, который генерирует XML, содержащий маркер карты.данные.(WordPress-запрос использует post__in и posts_per_page -1 для отображения информации для всех идентификаторов, сгенерированных запросом

ПРОБЛЕМА

В двух словах, каждая реализация формулы ХаверсайнаМне кажется, что это приводит к отсутствующим маркерам - в частности, к любым маркерам, которые очень близки к введенным пользователем координатам (не знаю точно, но я думаю, что они находятся в пределах 500 м). Это большая проблема, как будто пользователь вводит своипочтовый индекс, и магазин находится очень близко к их местоположению, он не будет отображаться.

Я пробовал около 8 различных перестановок форума, которые я выкопал из различных уроков с одинаковыми результатами.это формула, которую я сейчас использую на сайте, которая предоставляет все маркеры, за исключением тех, которые очень близки к позиции, введенной пользователями:

$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'];
}

Была одна формула, которую я попробовал, которая была предложена Майком Пеллиздесь: запрос геолокации SQL не находит точное местоположение

эта формулаo показывать маркеры, которые были очень близки к введенному пользователем местоположению, но пропускали другие, которые должны были отображаться в заданном радиусе.Чтобы устранить путаницу, я использовал следующий код:

$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'];
}

ЗАПРОС

В основном я хотел бы знать, почему ни один из этих блоков кода не отображаетсяправильные маркеры.Если кто-то может предложить какие-либо улучшения в коде или может указать мне на какой-то ресурс, который я мог упустить, было бы замечательно

РЕДАКТИРОВАТЬ

Думаю, мой psudeo ответ работал, но, как оказалось, проблемы все еще были.В итоге я пошел совсем по-другому, и я использую очень хороший локатор магазина jquery, который можно найти здесь: http://www.bjornblog.com/web/jquery-store-locator-plugin

Не подойдет для каждого проекта, кроме моегонужно, чтобы это было идеально (и работает!)

Ответы [ 5 ]

2 голосов
/ 07 января 2012

EDIT Этот искатель местоположения появляется достаточно часто, и я написал об этом статью.

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

Исходное сообщение

Давайте начнем с того, что раз и навсегда разберемся с формулой haversine, поместив ее в хранимую функцию, чтобы мы могли забыть о ее мрачных деталях. ПРИМЕЧАНИЕ: все это решение в уставных милях.

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 ;

Теперь давайте составим запрос, который ищет в ограничительной рамке, а затем уточнит поиск с помощью нашей функции расстояния и порядков по расстоянию

На основе PHP-кода в вашем вопросе:

Предположим, $radius - ваш радиус, $center_lat, $center_lng - ваша контрольная точка.

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

Обратите внимание на несколько вещей.

Во-первых, он выполняет вычисления ограничивающего прямоугольника в SQL, а не в PHP. Для этого нет веской причины, кроме как хранить все вычисления в одной среде. (radius / 69) - это количество градусов в radius статутных милях.

Во-вторых, он не зависит от размера продольной ограничительной рамки в зависимости от широты. Вместо этого он использует более простую, но немного слишком большую ограничивающую рамку. Эта ограничительная рамка захватывает несколько дополнительных записей, но измерение расстояния избавляет от них. Для вашего типичного приложения поиска почтового индекса / магазина разница в производительности незначительна. Если бы вы искали еще много записей (например, в базе данных всех полюсов электропитания), это могло бы быть не так тривиально.

В-третьих, он использует вложенный запрос для устранения расстояния, чтобы избежать необходимости запускать функцию расстояния более одного раза для каждого элемента.

В-четвертых, он заказывает по расстоянию по возрастанию. Это означает, что ваши результаты с нулевым расстоянием должны отображаться первыми в наборе результатов. Обычно имеет смысл сначала перечислить ближайшие вещи.

В-пятых, используется FLOAT вместо DOUBLE. Для этого есть веская причина. Формула расстояния хаверсин не идеальна, потому что она приближается к тому, что земля - ​​идеальная сфера. Это приближение нарушается примерно с тем же уровнем точности, что и эпсилон для FLOAT чисел. Таким образом, DOUBLE является обманчивым численным перебором для этой проблемы. (Не используйте эту формулу haversine для выполнения строительных работ, таких как дренаж на стоянке, или вы получите большие лужи, пару эпсилон, глубиной несколько дюймов, обещаю.) Это хорошо для приложений поиска магазинов.

В-шестых, вы определенно захотите создать индекс для столбца lat. Если ваша таблица местоположений меняется не очень часто, это поможет также создать индекс для вашего столбца lng. Но ваш индекс lat даст вам большую часть прироста производительности запросов.

Наконец, я протестировал хранимую процедуру и SQL, но не PHP.

Ссылка: http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL Также мой опыт работы с кучей бесконтактных искателей для медицинских учреждений.

--------------- РЕДАКТИРОВАТЬ --------------------

Если у вас нет пользовательского интерфейса, позволяющего определить хранимую процедуру, это неприятно. В любом случае, PHP позволяет использовать пронумерованные параметры в вызове sprintf, поэтому вы можете сгенерировать целое вложенное выражение, как это. ПРИМЕЧАНИЕ. Вам может потребоваться% $ 1f и т. Д. Вам нужно будет поэкспериментировать с этим.

$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 голосов
/ 09 января 2012

Немного подумав, я нашел «своего рода» решение проблемы отсутствующих маркеров.Два уравнения, которые я выложил, изначально дали правильные результаты, но в каждом из них отсутствовали либо маркеры, близкие к цели, либо по краям радиуса поиска.

Это не очень элегантно, но я подумал, что выполнение обоих уравнений и создание 2 массивов, которыеЗатем я объединил (удалив все дубликаты) и дал бы мне все нужные мне маркеры.Это работает (очевидно, производительность снижается, но это не приложение с высоким трафиком), поэтому я пока поработаю с этим, но я все еще ищу более практичное решение, если у кого-то оно есть!

0 голосов
/ 08 января 2012

Это код из работающей производственной системы,

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

Используется формула с другим расстоянием, но для локатора магазина разница минимальна.

0 голосов
/ 08 января 2012

Вы можете попробовать мой класс на http://www.phpclasses.org/package/6202-PHP-Generate-points-of-an-Hilbert-curve.html.. Он использует формулу Харвинса и кривую Гильберта для вычисления четырехугольника.Затем вы можете искать квадрик слева направо.Каждая позиция ключа - это точка на кривой монстра.Лучшее объяснение этой кривой можно найти в блоге Ника о пространственном индексе квадри Гильберта.Это похоже на использование расширения пространственного индекса из mysql, но у вас больше контроля.Вы можете использовать кривую az или кривую Мура, или вы можете изменить внешний вид.

0 голосов
/ 07 января 2012

Вот решение, которое я некоторое время успешно использовал в своих собственных расчетах гео-близости:

/**
 * 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
*/
...