【问题标题】:Missing results due to geo proximity formula (store locator)由于地理位置接近公式(商店定位器)而缺少结果
【发布时间】:2012-02-02 09:42:45
【问题描述】:

好的 - 我断断续续地为此苦苦挣扎了大约 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 公式的每个实现似乎都会导致缺少标记 - 特别是任何非常接近用户输入坐标的标记(不确切知道,但我认为它在大约 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 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 的内置地理空间功能是否有原因? dev.mysql.com/doc/refman/5.0/en/…
  • 我很难理解您代码中的一些内容。为什么HAVING 而不是WHERE? (float) $center_lat - ( (int) $radius / 69 ); 中的 int 和其他查询中的 truncate 是怎么回事?应用公式时请记住这一事实:赤道的一分钟经度定义为海里。整数度数是 60 海里。最后,尝试摆脱 BETWEEN 并改用 'WHERE a>=lat1 AND a
  • @Kenneth - 我不使用地理空间查询有几个原因。首先,我使用geo data store plugin 创建和维护我的标记数据表。这个插件生成我上面展示的表结构。其次,我遇到的用于创建商店定位器类型地图的绝大多数教程似乎都推荐一种类似于所列表结构的表结构。也许这些不是最好的理由,但我已经用当前的设置走了这么远,我很确定我想做的事情应该是可能的。
  • @OllieJones 欣赏这些提示。 HAVING(而不是 WHERE 似乎出现在我遇到的几乎所有公式中(包括 google maps tutorial 之一。int 的东西可能不需要在那里 - 我是变得如此沮丧以至于我开始留下一些可能已经从我发现的公式中删除的东西(无论哪种方式似乎都没有对结果产生任何影响。我尝试了你的BETWEEN/WHERE 建议,但我'恐怕它破坏了查询 - 请参阅下一条评论了解我的尝试:
  • WHERE lat &gt;=".$lat1." AND lat&lt;=".$lat2." AND lng &gt;=".$lng1." AND lat&lt;=".$lng2."

标签: php mysql wordpress geo proximity


【解决方案1】:

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

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。这是有充分理由的。半正弦距离公式并不完美,因为它近似认为地球是一个完美的球体。该近似值恰好以与 FLOAT 数字的 epsilon 大致相同的准确度水平分解。所以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);

【讨论】:

  • 感谢奥利非常详细的回答。但是在实施它来测试它时遇到了一些麻烦。很抱歉很痛苦,但我真的不熟悉 MySQL 存储过程(我可以使用 PhpMyAdmin 处理 MySQL 数据库,但这就是我的 MySQL 知识枯竭的地方)。浏览了一些关于使用此 GUI 创建存储函数的教程,但没有找到任何可以指导我完成的教程。 hasrsine 公式是这种格式是否至关重要,或者它是否可以包含在 php 查询的其余部分中?
  • 感谢 Ollie 的编辑 - 感谢您将其转换为 php 以及迄今为止的所有帮助。不幸的是,它似乎根本没有产生任何 ID 值。加倍检查代码以确保我没有做任何愚蠢的事情 - 什么都看不到。还尝试了您在 sprintf 中更改类型说明符的建议,但没有更改。不确定您的代码中的小写“a”是故意的还是拼写错误,但无论哪种方式都没有产生任何结果。
  • 我已将赏金授予此答案,因为它是最完整且解释最充分的答案(尽管最终并未实际使用它-请参阅下面我自己的答案)现在)。也就是说,这个问题产生了一些非常好的答案,我相信下面列出的方程式可能适用于其他人的项目,这些项目的设置与我的不同,所以它们都值得一看。
【解决方案2】:

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

/**
 * 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。不幸的是,虽然它通常有效,但我仍然遇到了同样的缺失标记问题。不过感谢您的尝试!
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-10-22
        • 1970-01-01
        • 2011-04-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多