寻找距离为“X”公里(或英里)内的城市

12

如果我的回答不够清晰,请在评论中告诉我或者需要更多的信息。也许已经有PHP中解决我所需的问题的方法了。

我正在寻找一个函数,可以从经度或纬度值中加减距离。

原因:我有一个包含所有纬度和经度的数据库,并想形成一个查询来提取X公里(或英里)内的所有城市。我的查询可能会像这样:

Select * From Cities Where (Longitude > X1 and Longitude < X2) And (Latitude > Y1 and Latitude < Y2)

 Where X1 = Longitude - (distance)
 Where X2 = Longitude + (distance)

 Where Y1 = Latitude - (distance)
 Where Y2 = Latitude + (distance)

我正在使用PHP和MySql数据库。

也欢迎任何建议! :)


你总是可以自己推导出这个函数... 这似乎是高中水平的微积分,或者如果你真的简化它的话,甚至是三角学... - rmeador
3
你可能会认为地球是一个完美的球体,但实际上不是这样。在赤道上每经过1度经线和其他地方每经过1度经线之间的差异是惊人的大。这绝对不像我们所希望的那样简单! - Andy Mikula
(see my answer below :)) - Andy Mikula
1
@ Mike,你不能仅仅添加一个标量距离来得到一个新的经度和纬度。由于地球的曲率,你需要指定角度。你说你想找到X公里的城市,我的解决方案可以做到这一点。 - Unknown
10个回答

17

这是一个MySQL查询语句,可以完全满足您的要求。请注意,像这样的东西通常是近似值,因为地球不是完美的球体,也没有考虑到山脉、丘陵、山谷等因素。我们在AcademicHomes.com上使用PHP和MySQL代码,返回距离$latitude、$longitude $radius英里范围内的记录。

$res = mysql_query("SELECT
    * 
FROM
    your_table
WHERE
    (
        (69.1 * (latitude - " . $latitude . ")) * 
        (69.1 * (latitude - " . $latitude . "))
    ) + ( 
        (69.1 * (longitude - " . $longitude . ") * COS(" . $latitude . " / 57.3)) * 
        (69.1 * (longitude - " . $longitude . ") * COS(" . $latitude . " / 57.3))
    ) < " . pow($radius, 2) . " 
ORDER BY 
    (
        (69.1 * (latitude - " . $latitude . ")) * 
        (69.1 * (latitude - " . $latitude . "))
    ) + ( 
        (69.1 * (longitude - " . $longitude . ") * COS(" . $latitude . " / 57.3)) * 
        (69.1 * (longitude - " . $longitude . ") * COS(" . $latitude . " / 57.3))
    ) ASC");

如果您不介意我问一下Keith,您在此查询中的索引设置是什么?这很好用,但它正在扫描数据库中每一行具有Lat/Long键的内容。 - Kladskull
3
嗨,迈克,如果性能很重要,我会在“纬度”和“经度”上设置索引,并使用BETWEEN子句调整上述查询,以将查询限制为较小的记录子集。添加以下内容:WHERE 纬度 BETWEEN $latitude - ($radius / 70) AND $latitude + ($radius / 70) AND 经度 BETWEEN $longitude - ($radius / 70) AND $longitude + ($radius / 70) 这将使数据库使用纬度和经度的索引。常数70是因为1度纬度或经度最大距离约为70英里。 - Keith Palmer Jr.
1
在我们老化的开发服务器上,有一个包含全球290万个城市/城镇的表格,在没有经纬度索引的情况下,查询时间为10.9秒;而在有经纬度索引的情况下,查询时间为0.52秒。 - Keith Palmer Jr.
使用空间索引快速定位附近的城市。对于这种工作,比普通索引快得多。 - Will
嗨,Keith,我错过了这篇文章(因为看到了你的原始答案,它非常有效)。我们的数据库现在大约有2.8百万条记录,并回来添加了以下内容:$longitude_rectangle1 = $longitude - $distance / abs(cos(deg2rad($latitude))*69); $longitude_rectangle2 = $longitude + $distance / abs(cos(deg2rad($latitude))*69); $latitude_rectangle1 = $latitude - ($distance/69); $latitude_rectangle2 = $latitude + ($distance/69); 我用这些来处理中间值-看起来差不多吧? - Kladskull

3
编辑:如果您在某个地方有一个包含所有城市及其纬度和经度值的列表,您可以进行查找。 在这种情况下,请参阅下面的第一个链接,了解计算纬度alt text的一度宽度的公式:

alt text

老实说,这个问题背后的复杂性是如此之大,以至于您最好使用Google Maps等服务来获取数据。 具体而言,地球不是一个完美的球体,两度之间的距离会随着您离赤道越近/越远而变化。

请参见http://en.wikipedia.org/wiki/Geographic_coordinate_system,了解我所说的内容,并查看Google Maps API


这基本上是我建议他推导的函数。我认为地球的轻微不规则形状在任何合理的距离上都不会有影响……如果我没记错,极地和赤道之间的距离差大约是50英里,在地球尺寸的范围内算不了什么。 - rmeador
然而,在计算表面位置时,这会产生很大的影响。 - Andy Mikula

1

我尝试使用上面的代码,但当点之间的距离在20-30英里范围内时,答案误差太大了,但我可以接受几英里的误差。与我的一个地图伙伴交谈后,我们想出了这个解决方案。代码是使用Python编写的,但您可以很容易地将其翻译成其他语言。为了避免不断转换为弧度,我重做了我的数据库,将纬度/经度点从度转换为弧度。好处在于大部分计算只需要进行一次。

ra = 3963.1906 # radius @ equator in miles, change to km  if you want distance in km
rb = 3949.90275  # radius @ poles in miles, change to km  if you want distance in km
ra2 = ra * ra
rb2 = rb * rb

phi = self.lat

big_ol_constant = (math.pow(ra2*math.cos(phi), 2) + pow(rb2*math.sin(phi), 2))/ (pow(ra*math.cos(phi), 2) + pow(rb*math.sin(phi), 2))

sqlWhere = "%(distance)g > sqrt((power(lat - %(lat)g,2) + power(lng-%(lng)g,2)) * %(big_ol_constant)g)" % {
    'big_ol_constant': big_ol_constant, 'lat': self.lat, 'lng': self.lng, 'distance': distance}

# This is the Django portion of it, where the ORM kicks in.  sqlWhere is what you would put after the WHERE part of your SQL Query.
qs = ZipData.objects.extra(where=[sqlWhere]);

当距离较小时,似乎非常准确,而在距离增加到200英里时(当然到那时,你会遇到“直线距离”与“铺设道路”的问题)。

这是我上面提到的ZipData模型。

class ZipData(models.Model):
    zipcode = ZipCodeField(null=False, blank=False, verbose_name="ZipCode", primary_key=True)
    city = models.CharField(max_length=32, null=False, blank=False)
    state = models.CharField(max_length=2)
    lat = models.FloatField(null=False, blank=False)
    lng = models.FloatField(null=False, blank=False)

另外需要注意的是,您可以在GeoNames.org上获取大量与邮政编码相关的地理数据,他们甚至还提供了一些可供使用的Web服务API。

这太棒了 Mark0978。你有完整的代码或 Django 应用程序可以共享作为即插即用模块吗? - Avid Coder
我现在已经包含了模型对象。你可以给我发邮件,我的用户名是@ gmail.com,我会把组成该应用程序的文件发送给你,但我不认为它们是可重复使用的应用程序。 - boatcoder

1

根据您包含的城市数量,您可以预先计算列表。我们在这里为内部应用程序执行此操作,其中+100m的不准确度对于我们的设置来说太多了。它通过具有位置1、位置2、距离的两个关键表来工作。然后,我们可以非常快速地从位置1返回距离x位置。

由于计算可以离线完成,因此不会影响系统的运行。用户还可以获得更快的结果。


0

使用以下URL中的设置,我构建了下面的查询。(请注意,我正在使用codeIgnitor查询数据库)

http://howto-use-mysql-spatial-ext.blogspot.com/2007/11/using-circular-area-selection.html

function getRadius($point="POINT(-29.8368 30.9096)", $radius=2)
{
    $km = 0.009;
    $center = "GeomFromText('$point')";
    $radius = $radius*$km;
    $bbox = "CONCAT('POLYGON((',
        X($center) - $radius, ' ', Y($center) - $radius, ',',
        X($center) + $radius, ' ', Y($center) - $radius, ',',
        X($center) + $radius, ' ', Y($center) + $radius, ',',
        X($center) - $radius, ' ', Y($center) + $radius, ',',
        X($center) - $radius, ' ', Y($center) - $radius, '
    ))')";

    $query = $this->db->query("
    SELECT id, AsText(latLng) AS latLng, (SQRT(POW( ABS( X(latLng) - X({$center})), 2) + POW( ABS(Y(latLng) - Y({$center})), 2 )))/0.009 AS distance
    FROM crime_listing
    WHERE Intersects( latLng, GeomFromText($bbox) )
    AND SQRT(POW( ABS( X(latLng) - X({$center})), 2) + POW( ABS(Y(latLng) - Y({$center})), 2 )) < $radius
    ORDER BY distance
        ");

    if($query->num_rows()>0){
        return($query->result());
    }else{
        return false;
    }
}

0
下面的函数来自于 nerddinner 的数据库(MSSQL),该数据库是 ASP.NET MVC 示例应用程序,可在 codeplex 上获取。
ALTER FUNCTION [dbo].[DistanceBetween] (@Lat1 as real,
                @Long1 as real, @Lat2 as real, @Long2 as real)
RETURNS real
AS
BEGIN

DECLARE @dLat1InRad as float(53);
SET @dLat1InRad = @Lat1 * (PI()/180.0);
DECLARE @dLong1InRad as float(53);
SET @dLong1InRad = @Long1 * (PI()/180.0);
DECLARE @dLat2InRad as float(53);
SET @dLat2InRad = @Lat2 * (PI()/180.0);
DECLARE @dLong2InRad as float(53);
SET @dLong2InRad = @Long2 * (PI()/180.0);

DECLARE @dLongitude as float(53);
SET @dLongitude = @dLong2InRad - @dLong1InRad;
DECLARE @dLatitude as float(53);
SET @dLatitude = @dLat2InRad - @dLat1InRad;
/* Intermediate result a. */
DECLARE @a as float(53);
SET @a = SQUARE (SIN (@dLatitude / 2.0)) + COS (@dLat1InRad)
                 * COS (@dLat2InRad)
                 * SQUARE(SIN (@dLongitude / 2.0));
/* Intermediate result c (great circle distance in Radians). */
DECLARE @c as real;
SET @c = 2.0 * ATN2 (SQRT (@a), SQRT (1.0 - @a));
DECLARE @kEarthRadius as real;
/* SET kEarthRadius = 3956.0 miles */
SET @kEarthRadius = 6376.5;        /* kms */

DECLARE @dDistance as real;
SET @dDistance = @kEarthRadius * @c;
return (@dDistance);
END

我猜这可能会有帮助。


地球的半径并不是恒定的 - 这个公式可以让你接近,但(至少对于我的以前的应用程序来说)还不够准确。如果你不担心有几百英里的差距,那么这是一个不错的选择 :) - Andy Mikula

0

你可以使用勾股定理来计算两对纬度/经度点之间的距离。

如果你有两个位置(Alpha和Beta),你可以使用以下公式来计算它们之间的距离:

SQRT( POW(Alpha_lat - Beta_lat,2) + POW(Alpha_lon - Beta_lon,2) )

3
对于短距离来说这很好,但我们的地球并不是平的。 - TalkingCode

0

有很多(糟糕的选择)

  • 使用数学公式计算距离(将X1-X2和Y1-Y2视为向量)。

  • 预先创建一个查找表,包含所有组合并保存距离。

  • 考虑使用MySQL的GIS特定扩展。这里是我找到的一篇文章


0

lessthandot.com实际上有3种不同的方法来做到这一点。您需要稍微浏览一下博客,但它们都在那里。 http://blogs.lessthandot.com/


0
不要重复造轮子。这是一个空间查询。使用MySQL内置的空间扩展将纬度-经度坐标数据存储在本地MySQL几何列类型中。然后使用距离函数查询彼此之间距离在指定范围内的点。
免责声明:这是基于阅读文档,我自己没有尝试过。

从您链接的页面顶部:目前,MySQL没有按规范实现这些函数。那些已经实现的函数返回与相应的基于MBR的函数相同的结果。这包括以下列表中除Distance()和Related()之外的函数。 - boatcoder
1
@Mark0978 页面确实包含了那个免责声明,但是在我的回答中我推荐使用“Distance”函数。正如你评论中引用的那样,该免责声明不适用于“Distance”函数。因此,该免责声明与我的回答无关。 - MarkJ

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