SQlite Получение ближайших мест (с широтой и долготой)
У меня есть данные с широтой и долготой, хранящиеся в моей базе данных SQLite, и я хочу получить ближайшие местоположения к параметрам, которые я ввел (например, мое текущее местоположение - lat/lng и т.д.).
Я знаю, что это возможно в MySQL, и я провел довольно некоторое исследование, что SQLite нуждается в пользовательской внешней функции для формулы Хаверсина (вычисление расстояния на сфере), но я не нашел ничего, что написано в Java и работает.
Кроме того, если я хочу добавить пользовательские функции, мне нужен org.sqlite
.jar(для org.sqlite.Function
) и добавляет ненужный размер в приложение.
Другая сторона этого - мне нужен порядок по функциям из SQL, потому что отображение расстояния в одиночку - это не большая проблема - я уже сделал это в своем обычном SimpleCursorAdapter, но я не могу сортировать данные, потому что у меня нет столбца расстояния в моей базе данных. Это означало бы обновление базы данных каждый раз при изменении местоположения, а также от потери батареи и производительности. Поэтому, если кто-то имеет идею по сортировке курсора с столбцом, который не находится в базе данных, я был бы признателен тоже!
Я знаю, что есть множество приложений для Android, которые используют эту функцию, но кто-то может объяснить магию.
Кстати, я нашел эту альтернативу: Запрос для получения записей на основе Radius в SQLite?
Предлагая сделать 4 новых столбца для значений cos и sin для lat и lng, но есть ли другой, не слишком избыточный способ?
Ответы
Ответ 1
1). Сначала фильтруйте данные SQLite с хорошим приближением и уменьшайте количество данных, которые необходимо оценить в вашем Java-коде. Для этой цели используйте следующую процедуру:
Чтобы иметь детерминированный порог и более точный фильтр для данных, лучше рассчитать 4 местоположения, которые находятся в метре radius
на севере, западе, востоке и южнее вашей центральной точки в вашем java-коде, а затем легко проверяйте меньше и больше, чем операторы SQL ( > , <), чтобы определить, являются ли ваши точки в базе данных в этом прямоугольнике или нет.
Метод calculateDerivedPosition(...)
вычисляет эти точки для вас (p1, p2, p3, p4 на картинке).
![enter image description here]()
/**
* Calculates the end-point from a given source at a given range (meters)
* and bearing (degrees). This methods uses simple geometry equations to
* calculate the end-point.
*
* @param point
* Point of origin
* @param range
* Range in meters
* @param bearing
* Bearing in degrees
* @return End-point from the source given the desired range and bearing.
*/
public static PointF calculateDerivedPosition(PointF point,
double range, double bearing)
{
double EarthRadius = 6371000; // m
double latA = Math.toRadians(point.x);
double lonA = Math.toRadians(point.y);
double angularDistance = range / EarthRadius;
double trueCourse = Math.toRadians(bearing);
double lat = Math.asin(
Math.sin(latA) * Math.cos(angularDistance) +
Math.cos(latA) * Math.sin(angularDistance)
* Math.cos(trueCourse));
double dlon = Math.atan2(
Math.sin(trueCourse) * Math.sin(angularDistance)
* Math.cos(latA),
Math.cos(angularDistance) - Math.sin(latA) * Math.sin(lat));
double lon = ((lonA + dlon + Math.PI) % (Math.PI * 2)) - Math.PI;
lat = Math.toDegrees(lat);
lon = Math.toDegrees(lon);
PointF newPoint = new PointF((float) lat, (float) lon);
return newPoint;
}
И теперь создайте свой запрос:
PointF center = new PointF(x, y);
final double mult = 1; // mult = 1.1; is more reliable
PointF p1 = calculateDerivedPosition(center, mult * radius, 0);
PointF p2 = calculateDerivedPosition(center, mult * radius, 90);
PointF p3 = calculateDerivedPosition(center, mult * radius, 180);
PointF p4 = calculateDerivedPosition(center, mult * radius, 270);
strWhere = " WHERE "
+ COL_X + " > " + String.valueOf(p3.x) + " AND "
+ COL_X + " < " + String.valueOf(p1.x) + " AND "
+ COL_Y + " < " + String.valueOf(p2.y) + " AND "
+ COL_Y + " > " + String.valueOf(p4.y);
COL_X
- это имя столбца в базе данных, в котором хранятся значения широты, а COL_Y
- для долготы.
Итак, у вас есть некоторые данные, которые находятся вблизи вашей центральной точки с хорошим приближением.
2) Теперь вы можете зацикливаться на этих отфильтрованных данных и определить, действительно ли они находятся рядом с вашей точкой (в круге) или нет, используя следующие методы:
public static boolean pointIsInCircle(PointF pointForCheck, PointF center,
double radius) {
if (getDistanceBetweenTwoPoints(pointForCheck, center) <= radius)
return true;
else
return false;
}
public static double getDistanceBetweenTwoPoints(PointF p1, PointF p2) {
double R = 6371000; // m
double dLat = Math.toRadians(p2.x - p1.x);
double dLon = Math.toRadians(p2.y - p1.y);
double lat1 = Math.toRadians(p1.x);
double lat2 = Math.toRadians(p2.x);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2)
* Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double d = R * c;
return d;
}
Наслаждайтесь!
Я использовал и настроил эту ссылку и выполнил ее.
Ответ 2
Ответ Криса действительно полезен (спасибо!), но будет работать, только если вы используете прямолинейные координаты (например, UTM или ссылки на сетку ОС). Если вы используете градусы для lat/lng (например, WGS84), то вышеупомянутое работает только на экваторе. В других широтах вам необходимо уменьшить влияние долготы на порядок сортировки. (Представьте, что вы близко к северному полюсу... градус широты все тот же, что и в любом месте, но степень долготы может быть всего в нескольких футах. Это будет означать, что порядок сортировки неверен).
Если вы не находитесь на экваторе, предварительно вычислите коэффициент fudge, исходя из вашей текущей широты:
<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);
Затем порядок:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)
Это все еще только приближение, но намного лучше первого, поэтому погрешности порядка сортировки будут намного реже.
Ответ 3
Я знаю, что на это был дан ответ и принят, но я подумал, что добавлю свой опыт и решение.
В то время как я был счастлив сделать функцию хаверинса на устройстве для вычисления точного расстояния между текущей позицией пользователя и любым конкретным целевым местоположением, необходимо было отсортировать и ограничить результаты запроса в порядке расстояния.
Менее удовлетворительным решением является возвращение партии, сортировка и фильтрация после факта, но это приведет к тому, что второй курсор и многие ненужные результаты будут возвращены и отброшены.
Мое предпочтительное решение состояло в том, чтобы передать порядок сортировки квадратов дельта-значений long и lats:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))
Нет необходимости делать полный haversine только для порядка сортировки, и нет необходимости в квадрате корни результатов, поэтому SQLite может обрабатывать вычисления.
EDIT:
Этот ответ по-прежнему получает любовь. Он работает отлично в большинстве случаев, но если вам нужна немного больше точности, пожалуйста, проверьте ответ @Teasel, ниже которого добавлен фактор "fudge", который фиксирует неточности, которые увеличиваются по мере приближения широты 90.
Ответ 4
Считаете ли вы Geohash тег/индекс для своих записей, чтобы уменьшить размер вашего результата установите, а затем примените соответствующую функцию.
Другой вопрос, связанный с stackoverflow в подобной области:
finding-the-closest-point-to-a-given-point
Ответ 5
Чтобы увеличить производительность, я предлагаю улучшить идею @Chris Simpson со следующим предложением ORDER BY
:
ORDER BY (<L> - <A> * LAT_COL - <B> * LON_COL + LAT_LON_SQ_SUM)
В этом случае вы должны передать следующие значения из кода:
<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
<B> = 2 * center_lon
И вы также должны сохранить LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2
в качестве дополнительного столбца в базе данных. Заполните его, добавив свои объекты в базу данных. Это немного улучшает производительность при извлечении большого объема данных.
Ответ 6
Взгляните на это сообщение:
Функция расстояния для sqlite
Кажется, вы можете добавить пользовательскую функцию Distance() в SQLite, которая может позволить вам избежать перескакивания всех обручей в других ответах.
Ответ 7
Попробуйте что-то вроде этого:
//locations to calculate difference with
Location me = new Location("");
Location dest = new Location("");
//set lat and long of comparison obj
me.setLatitude(_mLat);
me.setLongitude(_mLong);
//init to circumference of the Earth
float smallest = 40008000.0f; //m
//var to hold id of db element we want
Integer id = 0;
//step through results
while(_myCursor.moveToNext()){
//set lat and long of destination obj
dest.setLatitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE)));
dest.setLongitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE)));
//grab distance between me and the destination
float dist = me.distanceTo(dest);
//if this is the smallest dist so far
if(dist < smallest){
//store it
smallest = dist;
//grab it id
id = _myCursor.getInt(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_ID));
}
}
После этого идентификатор содержит элемент, который вы хотите получить из базы данных, чтобы вы могли его получить:
//now we have traversed all the data, fetch the id of the closest event to us
_myCursor = _myDBHelper.fetchID(id);
_myCursor.moveToFirst();
//get lat and long of nearest location to user, used to push out to map view
_mLatNearest = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE));
_mLongNearest = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE));
Надеюсь, что это поможет!