Сортировка запроса MySQL по широте/долготе

Каждый пользователь в моей базе данных имеет свою широту и долготу, хранящиеся в двух полях (lat, lon)

Формат каждого поля:

lon | -1.403976 
lat | 53.428691

Если пользователь ищет других пользователей, скажем 100 миль, я выполняю следующее, чтобы вычислить соответствующий диапазон lat/lon ($ lat и $lon - текущие значения пользователей)

$R = 3960;  // earth 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'";

Это прекрасно работает, и я использую функцию для вычисления и отображения фактического расстояния между пользователями на выходном этапе, но я хотел бы иметь возможность сортировать результаты, уменьшая или увеличивая расстояние на этапе запроса.

Есть ли способ сделать это?

Ответ 1

Помните Пифагора?

$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))";

Ответ 2

Использование только SELECT * 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";

Вы можете легко превратить это в класс, который принял Radians или Degrees из приведенной выше информации.

Ответ 3

Это формула, которая дала мне правильные результаты (в отличие от вышеприведенных решений). Подтверждено с помощью функции "Измерение расстояния" в Google Картах (прямое расстояние, а не расстояние транспортировки).

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. Вы можете заменить их фактическими значениями, если хотите. latitude и longitude - имена столбцов.

3959 - радиус Земли в милях; выход distance также будет в милях. Чтобы изменить его на километры, замените 3959 на 6371.

Ответ 4

не даст вам результатов, упорядоченных по плоской дистанции (не считая кривизны земли), но для малого радиуса "должен работать".

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);

Ответ 5

Ни один из приведенных выше ответов не работает правильно через гринвичский меридиан. Формула Haversine:

  // 6371 is the Earth 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

который я взял отсюда и на который есть ссылка в этом ответе на похожий вопрос, работает.