Каждый пользователь в моей базе данных имеет свою широту и долготу, хранящиеся в двух полях (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'";
Это прекрасно работает, и я использую функцию для вычисления и отображения фактического расстояния между пользователями на выходном этапе, но я хотел бы иметь возможность сортировать результаты, уменьшая или увеличивая расстояние на этапе запроса.
Есть ли способ сделать это?
Помните Пифагора?
$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
.