14

我数据库中的每个用户都将他们的纬度和经度存储在两个字段中(纬度,经度)

每个字段的格式为:

lon | -1.403976 
lat | 53.428691

如果用户在 100 英里范围内搜索其他用户,我执行以下操作以计算适当的纬度/经度范围($lat 和 $lon 是当前用户值)

$R = 3960;  // earth's mean radius
$rad = '100';
// first-cut bounding box (in degrees)
$maxLat = $lat + rad2deg($rad/$R);
$minLat = $lat - rad2deg($rad/$R);
// compensate for degrees longitude getting smaller with increasing latitude
$maxLon = $lon + rad2deg($rad/$R/cos(deg2rad($lat)));
$minLon = $lon - rad2deg($rad/$R/cos(deg2rad($lat)));

$maxLat=number_format((float)$maxLat, 6, '.', '');
$minLat=number_format((float)$minLat, 6, '.', '');
$maxLon=number_format((float)$maxLon, 6, '.', '');
$minLon=number_format((float)$minLon, 6, '.', '');

然后我可以执行如下查询:

$query = "SELECT * FROM table WHERE lon BETWEEN '$minLon' AND '$maxLon' AND lat BETWEEN '$minLat' AND '$maxLat'";

这很好用,我使用一个函数在输出阶段计算和显示用户之间的实际距离,但我希望能够通过在查询阶段减少或增加距离来对结果进行排序。

有没有办法做到这一点?

4

5 回答 5

40

还记得毕达哥拉斯吗?

$sql = "SELECT * FROM table 
    WHERE lon BETWEEN '$minLon' AND '$maxLon' 
      AND lat BETWEEN '$minLat' AND '$maxLat'
    ORDER BY (POW((lon-$lon),2) + POW((lat-$lat),2))";

从技术上讲,这是距离的平方,而不是实际距离,但是因为您只是将它用于排序,所以没关系。

这使用平面距离公式,该公式在小距离上应该很好。

然而:

如果您想更精确或使用更长的距离,请使用以下公式计算以弧度为单位的大圆距离

dist = acos[ sin(lat1)*sin(lat2)+cos(lat1)*cos(lat2)*cos(lng1-lng2) ]

(要以实际单位而不是弧度获得距离,请将其乘以地球的半径。不过,这对于订购目的不是必需的。)

MySQL 计算引擎假定纬度和经度以弧度为单位,因此如果它以度为单位(可能是),则必须将每个值乘以 pi/180,大约为 0.01745:

$sf = 3.14159 / 180; // scaling factor
$sql = "SELECT * FROM table 
    WHERE lon BETWEEN '$minLon' AND '$maxLon' 
      AND lat BETWEEN '$minLat' AND '$maxLat'
    ORDER BY ACOS(SIN(lat*$sf)*SIN($lat*$sf) + COS(lat*$sf)*COS($lat*$sf)*COS((lon-$lon)*$sf))";

甚至:

$sf = 3.14159 / 180; // scaling factor
$er = 6350; // earth radius in miles, approximate
$mr = 100; // max radius
$sql = "SELECT * FROM table 
    WHERE $mr >= $er * ACOS(SIN(lat*$sf)*SIN($lat*$sf) + COS(lat*$sf)*COS($lat*$sf)*COS((lon-$lon)*$sf))
    ORDER BY ACOS(SIN(lat*$sf)*SIN($lat*$sf) + COS(lat*$sf)*COS($lat*$sf)*COS((lon-$lon)*$sf))";
于 2013-05-09T15:50:41.243 回答
8

使用 justSELECT * FROM Table WHERE lat between $minlat and $maxlat不够准确。

查询距离的正确方法是使用弧度坐标。

<?php
  $sql = "SELECT * FROM Table WHERE acos(sin(1.3963) * sin(Lat) + cos(1.3963) * cos(Lat) * cos(Lon - (-0.6981))) * 6371 <= 1000";

这是一个方便的参考 - http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates

例如:

<?php
  $distance = 100;
  $current_lat = 1.3963;
  $current_lon = -0.6981;
  $earths_radius = 6371;

  $sql = "SELECT * FROM Table T WHERE acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius <= $distance";

如果您想按顺序进行排序并显示距离:

<?php
  $distance = 100;
  $current_lat = 1.3963;
  $current_lon = -0.6981;
  $earths_radius = 6371;

  $sql = "SELECT *, (acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius) as distance FROM Table T WHERE acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius <= $distance ORDER BY acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius DESC";

为@Blazemonger 编辑并避免疑问:) 如果你想以度数而不是弧度工作:

<?php
  $current_lat_deg = 80.00209691585;
  $current_lon_deg = -39.99818366895;
  $radians_to_degs = 57.2957795;

  $distance = 100;
  $current_lat = $current_lat_deg / $radians_to_degs;
  $current_lon = $current_lon_deg / $radians_to_degs;
  $earths_radius = 6371;

  $sql = "SELECT *, (acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius) as distance FROM Table T WHERE acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius <= $distance ORDER BY acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius DESC";

您可以轻松地将其包装成一个类,该类从上面提供的信息中接受弧度或度数。

于 2013-05-09T16:14:04.823 回答
5

这是给我正确结果的公式(与上面的解决方案相反)。使用谷歌地图“测量距离”功能确认(直接距离,而不是运输距离)。

SELECT
    *,
    ( 3959 * acos( cos( radians(:latitude) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians(:longitude) ) + sin( radians(:latitude) ) * sin( radians( latitude ) ) ) ) AS `distance`
FROM `locations`
ORDER BY `distance` ASC

:latitude并且:longitude是 PDO 函数的占位符。如果您愿意,可以将它们替换为实际值。latitudelongitude是列名。

3959是以英里为单位的地球半径;distance输出也将以英里为单位。要将其更改为公里,请替换39596371

于 2015-07-11T17:58:46.427 回答
0

不会给你按平面距离排序的结果(不考虑地球的曲率),但对于小半径'应该可以解决。

SELECT * from table where lon between '$minLon' and '$maxLon' and lat between '$minLat' and '$maxLat' order by (abs(lon-$lon)/2) + (abs(lat-$lat)/2);
于 2013-05-09T15:55:18.133 回答
0

上面的答案都不能在格林威治子午线上正确工作。哈弗辛公式:

  // 6371 is the Earth's radius in km
  6371 * 2 * ASIN(SQRT( 
     POWER(SIN((lat - abs(:latitude)) * pi()/180 / 2), 2) 
      + COS(lat * pi()/180 ) * COS(abs(:latitude) * pi()/180) 
      * POWER(SIN((lon - :longitude) *  pi()/180 / 2), 2) 
  )) as distance

我从这里拿来的并且在这个答案中引用了一个类似的问题,确实有效。

于 2019-01-28T15:17:16.747 回答