У меня есть данные с широтой и долготой, хранящиеся в моей базе данных SQLite, и я хочу получить ближайшие местоположения к параметрам, которые я ввел (например, мое текущее местоположение - lat/lng и т.д.).
Я знаю, что это возможно в MySQL, и я провел довольно некоторое исследование, что SQLite нуждается в пользовательской внешней функции для формулы Хаверсина (вычисление расстояния на сфере), но я не нашел ничего, что написано в Java и работает.
Кроме того, если я хочу добавить пользовательские функции, мне нужен org.sqlite
.jar(для org.sqlite.Function
) и добавляет ненужный размер в приложение.
Другая сторона этого - мне нужен порядок по функциям из SQL, потому что отображение расстояния в одиночку - это не большая проблема - я уже сделал это в своем обычном SimpleCursorAdapter, но я не могу сортировать данные, потому что у меня нет столбца расстояния в моей базе данных. Это означало бы обновление базы данных каждый раз при изменении местоположения, а также от потери батареи и производительности. Поэтому, если кто-то имеет идею по сортировке курсора с столбцом, который не находится в базе данных, я был бы признателен тоже!
Я знаю, что есть множество приложений для Android, которые используют эту функцию, но кто-то может объяснить магию.
Кстати, я нашел эту альтернативу: Запрос для получения записей на основе Radius в SQLite?
Предлагая сделать 4 новых столбца для значений cos и sin для lat и lng, но есть ли другой, не слишком избыточный способ?
1). Сначала фильтруйте данные SQLite с хорошим приближением и уменьшайте количество данных, которые необходимо оценить в вашем Java-коде. Для этой цели используйте следующую процедуру:
Чтобы иметь детерминированный порог и более точный фильтр для данных, лучше рассчитать 4 местоположения, которые находятся в метре radius
на севере, западе, востоке и южнее вашей центральной точки в вашем java-коде, а затем легко проверяйте меньше и больше, чем операторы SQL ( > , <), чтобы определить, являются ли ваши точки в базе данных в этом прямоугольнике или нет.
Метод calculateDerivedPosition(...)
вычисляет эти точки для вас (p1, p2, p3, p4 на картинке).
/**
* 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;
}
Наслаждайтесь!
Я использовал и настроил эту ссылку и выполнил ее.
COL_X
и COL_Y
в WHERE
. Это столбцы широты и долготы?
Я знаю, что на это был дан ответ и принят, но я подумал, что добавлю свой опыт и решение.
В то время как я был счастлив сделать функцию хаверинса на устройстве для вычисления точного расстояния между текущей позицией пользователя и любым конкретным целевым местоположением, необходимо было отсортировать и ограничить результаты запроса в порядке расстояния.
Менее удовлетворительным решением является возвращение партии, сортировка и фильтрация после факта, но это приведет к тому, что второй курсор и многие ненужные результаты будут возвращены и отброшены.
Мое предпочтительное решение состояло в том, чтобы передать порядок сортировки квадратов дельта-значений long и lats:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))
Нет необходимости делать полный haversine только для порядка сортировки, и нет необходимости в квадрате корни результатов, поэтому SQLite может обрабатывать вычисления.
EDIT:
Этот ответ по-прежнему получает любовь. Он работает отлично в большинстве случаев, но если вам нужна немного больше точности, пожалуйста, проверьте ответ @Teasel, ниже которого добавлен фактор "fudge", который фиксирует неточности, которые увеличиваются по мере приближения широты 90.
Ответ Криса действительно полезен (спасибо!), но будет работать, только если вы используете прямолинейные координаты (например, 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>)
Это все еще только приближение, но намного лучше первого, поэтому погрешности порядка сортировки будут намного реже.
Считаете ли вы Geohash тег/индекс для своих записей, чтобы уменьшить размер вашего результата установите, а затем примените соответствующую функцию.
Другой вопрос, связанный с stackoverflow в подобной области: finding-the-closest-point-to-a-given-point
Чтобы увеличить производительность, я предлагаю улучшить идею @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
в качестве дополнительного столбца в базе данных. Заполните его, добавив свои объекты в базу данных. Это немного улучшает производительность при извлечении большого объема данных.
Взгляните на это сообщение:
Кажется, вы можете добавить пользовательскую функцию Distance() в SQLite, которая может позволить вам избежать перескакивания всех обручей в других ответах.
Попробуйте что-то вроде этого:
//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));
Надеюсь, что это поможет!