Сортировка запроса 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
который я взял отсюда и на который есть ссылка в этом ответе на похожий вопрос, работает.