由于地理临近公式(门店定位器),导致结果缺失。

7

好的 - 我已经花了大约3个月的时间,在这期间一直在努力解决问题。由于我已经尽可能使用了各种地理接近度公式,但仍然没有得到正确的结果,因此我认为是时候寻求帮助了。

目标

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

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

这个查询的结果(文章id)被输入到一个WordPress查询中,生成包含地图标记数据的XML。(WordPress查询使用post__in和posts_per_page -1来显示由查询生成的所有ID的信息)
问题在于,我找到的Haversine公式的每个实现似乎都会导致缺少标记,特别是任何距离用户输入坐标非常接近的标记(不确定但我认为在大约500米内)。这是一个很大的问题,因为如果用户输入他们的邮政编码,并且有一家商店非常靠近他们的位置,它将不会显示出来。
我尝试了各种不同的公式排列组合,从各种教程中挖掘出来,但结果都相同。下面是我目前在网站上使用的公式,它提供了除那些非常接近用户输入位置的标记之外的所有标记:
$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'];
}

我尝试了Mike Pelley在这里建议的1个公式:Geolocation SQL query not finding 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

这并不适用于所有项目,但对于我的需求来说,它是完美的(而且有效!)


你不使用MySQL内置的地理空间功能,有什么原因吗? http://dev.mysql.com/doc/refman/5.0/en/creating-a-spatially-enabled-mysql-database.html - Kenneth
@OllieJones 感谢你的建议。在我遇到的几乎所有公式中(包括google maps tutorial中的公式),HAVING(而不是WHERE)似乎都会出现。int这些东西可能不需要,但是我变得很沮丧,开始将可以从我找到的公式中剥离的东西留下来(无论如何都没有对结果产生任何影响)。我尝试了你的BETWEEN/WHERE建议,但恐怕它破坏了查询——请参见我下一个评论中所尝试的内容: - FourStacks
您可能想要查看此问题的答案 https://dev59.com/hXRB5IYBdhLWcg3wl4Sc - Kenneth
我遇到了同样的问题。 - Mr B
@Sid - 请看下面答案中我的“类似”解决方案。虽然我确定这不是最有效的方法,但现在我已经让它完全按照预期工作了。下面还有一些其他非常好的答案,我相信它们应该也能起作用,但可能是由于我的其余设置问题,导致它们没有像应该那样准确。 - FourStacks
显示剩余6条评论
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。除了将所有计算放在一个环境中之外,没有其他好的理由。 "(半径/ 69)"是半径英里的度数。
其次,它不会根据纬度调整经度边界框的大小。相反,它使用一个更简单但略微过大的边界框。这个边界框捕获了一些额外的记录,但距离测量会去除它们。对于您典型的邮编/商店查找应用程序,性能差异可以忽略不计。如果您搜索的记录更多(例如,所有公用事业杆的数据库),则可能不那么微不足道。
第三,它使用嵌套查询来进行距离消除,以避免为每个项目运行距离函数超过一次。
第四,按距离升序排序。这意味着您的零距离结果应该首先出现在结果集中。通常有意义的是将最近的事物列在前面。
第五,它始终使用FLOAT而不是DOUBLE。有一个很好的理由。haversine距离公式并不完美,因为它做出了地球是一个完美球体的近似。这种近似在与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);

谢谢你的详细答复,奥利!但我还是有些麻烦,在实现和测试它方面。抱歉,我真的不熟悉MySQL存储过程(我可以在PhpMyAdmin中处理MySQL数据库,但这就是我的MySQL知识极限了)。我查找了一些关于使用此GUI创建存储函数的教程,但没有找到任何指导我完成此操作的东西。Haversine公式是否必须采用这种格式,或者可以将其包含在其他PHP查询中? - FourStacks
感谢Ollie的编辑 - 感谢您将其转换为PHP并迄今为止提供的所有帮助。不幸的是,似乎根本没有产生任何ID值。仔细检查了代码以确保我没有做任何愚蠢的事情 - 没有发现任何问题。还尝试了您在sprintf中更改类型说明符的建议,但没有任何变化。不确定您代码中的小写字母“a”是否是有意还是打字错误,但无论哪种方式都没有产生结果。 - FourStacks
我已经将赏金授予这个答案,因为它是最完整和详细解释的答案(尽管最终没有使用它 - 请参见我的答案,我现在正在使用它)。话虽如此,这个问题产生了一些非常好的答案,我相信下面列出的方程可能适用于其他人的项目,只要设置不同,所以它们都值得一看。 - FourStacks

0

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


0

稍微有些侧面思考,我想出了一个“类似”的解决方案来解决缺失标记的问题。最初我发布的两个方程式都给出了正确的结果,但是每个方程式都错过了靠近目标或在搜索半径边缘的标记。

这并不是非常优雅,但我想到运行这两个方程式并生成2个数组,然后将它们组合在一起(删除任何重复项),这样就可以得到我正在寻找的所有标记。这确实有效(显然会影响性能,但这不是高流量应用程序),所以我暂时会使用这个方法,但如果有更实用的解决方案,我仍然需要它!


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

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


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

感谢@Fleep发布代码。不幸的是,虽然它通常有效,但我仍然遇到了相同的缺失标记问题。还是很感激你的尝试! - FourStacks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接