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

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

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

lon | -1.403976 lat | 53.428691 

Если пользователь ищет других пользователей в пределах, скажем, 100 миль, я выполняю следующее, чтобы вычислить соответствующий диапазон lat / lon ($ 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'"; 

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

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

Solutions Collecting From Web of "Сортировка запроса MySQL по широте / долготе"

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

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

Использование только 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 из приведенной выше информации.

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

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

Это формула, которая дала мне правильные результаты (в отличие от вышеприведенных решений). Подтверждено с помощью функции «Измерение расстояния» в 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 .