7

好的 - 我断断续续地为此苦苦挣扎了大约 3 个月,因为我已经用尽了我遇到的所有地理接近度公式,而且我离得到正确的结果还差得远,我想是时候了寻求帮助。

目的

我正在建立一个相当基本的商店定位器实现。用户输入他们的邮政编码并从预定义的搜索半径列表中进行选择。gmaps API 为该地址生成纬度/经度坐标并将它们传递给 php 脚本。在此脚本中,针对 mysql 数据库表查询用户坐标(结构如下)

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

该查询的结果(post ids)被输入到 wordpress 查询中,该查询生成包含地图标记数据的 XML。(wordpress 查询使用 post__in 和 posts_per_page -1 显示查询生成的所有 ID 的信息

问题

简而言之,我遇到的 Haversine 公式的每个实现似乎都会导致缺少标记 - 特别是任何非常接近用户输入坐标的标记(不确切知道,但我认为它在大约 500m 之内)。这是一个大问题,因为如果用户输入他们的邮政编码并且有一家商店非常靠近他们的位置,它就不会出现。

我已经尝试了大约 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'];
}

我尝试了 1 个公式,这是 Mike Pelley 在这里建议的:Geolocation SQL query not found exact location

这个公式似乎显示了非常接近用户输入位置的标记,但错过了应该在给定半径内显示的其他标记。为了消除任何混淆,这是我使用的代码:

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

请求

基本上我想知道为什么这些代码块都没有显示正确的标记。如果有人可以建议对代码进行任何改进,或者可以将我指向一些我可能错过的资源,那就太好了

编辑

以为我的伪答案有效,但事实证明仍然存在问题。我现在最终选择了一个非常不同的方法,我正在使用一个非常好的 jquery 商店定位器,可以在这里找到:http ://www.bjornblog.com/web/jquery-store-locator-plugin

不适用于那里的每个项目,但对于我的需要,它是完美的(并且有效!)

4

5 回答 5

2

编辑 这个定位器经常出现,以至于我写了一篇关于它的文章。

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

原帖

让我们从一劳永逸地处理半正弦公式开始,将其放入存储函数中,这样我们就可以忘记它的繁琐细节。注意:整个解决方案都在法定里程中。

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

其次,它不会根据纬度调整纵向边界框的大小。相反,它使用了一个更简单但稍微太大的边界框。这个边界框捕获了一些额外的记录,但距离测量会消除它们。对于您典型的邮政编码/商店查找应用程序,性能差异可以忽略不计。如果您要搜索更多记录(例如,所有电线杆的数据库),它可能不会那么简单。

第三,它使用嵌套查询来进行距离消除,以避免对每个项目多次运行距离函数。

第四,它按距离 ASCENDING 排序。这意味着您的零距离结果应该首先出现在结果集中。首先列出最近的事物通常是有意义的。

第五,它使用FLOAT而不是DOUBLE贯穿始终。这是有充分理由的。半正弦距离公式并不完美,因为它近似认为地球是一个完美的球体。该近似值恰好在与数字 epsilon 大致相同的准确度水平上崩溃FLOAT。对于DOUBLE这个问题,欺骗性的数字过度杀伤力也是如此。(不要使用这个半正弦公式来做像停车场排水这样的土木工程工作,否则你会得到几个 epsilon 的大水坑,几英寸深,我保证。)它适用于商店查找应用程序。

第六,您肯定会想要为您的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);
于 2012-01-07T02:40:36.010 回答
0

这是我在自己的地理邻近度计算中成功使用了一段时间的解决方案:

/**
 * 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
*/
于 2012-01-06T20:57:51.710 回答
0

您可以在http://www.phpclasses.org/package/6202-PHP-Generate-points-of-an-Hilbert-curve.html尝试我的课程。它使用harvesine 公式和希尔伯特曲线来计算quadkey。然后,您可以从左到右搜索四键。键的每个位置都是怪物曲线上的一个点。可以在 Nick 的空间索引四叉树希尔伯特曲线博客中找到对该曲线的更好解释。这就像使用 mysql 的空间索引扩展,但你有更多的控制权。您可以使用 az 曲线或摩尔曲线,也可以更改外观。

于 2012-01-07T22:27:34.620 回答
0

这是来自工作生产系统的代码,

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

使用不同的距离公式,但对于商店定位器,差异很小。

于 2012-01-08T18:27:58.857 回答
0

稍微横向思考一下,我想出了一个解决缺失标记问题的“某种”解决方案。我最初发布的两个方程给出了正确的结果,但每个方程都错过了靠近目标或搜索半径边缘的标记

它不是很优雅,但我认为运行两个方程并生成 2 个数组,然后将它们组合(删除任何重复项)会给我所有我正在寻找的标记。这确实有效(显然会影响性能,但它不是一个高流量的应用程序)所以我会暂时使用它,但如果有人有一个更实用的解决方案,我仍然在寻求一个更实用的解决方案!

于 2012-01-08T22:35:39.287 回答