SQlite 获取最近的位置(带经纬度)

我把带有经纬度的数据存储在我的 SQLite 数据库中,我想得到与我输入的参数最接近的位置(ex。我当前的位置-lat/lng 等)。

我知道这在 MySQL 中是可能的,而且我做了一些研究,认为 SQLite 需要一个定制的外部函数来实现半正矢公式(计算球面上的距离) ,但是我还没有发现任何用 Java 编写并且可以工作的东西。

另外,如果我想添加自定义函数,我需要 org.sqlite。Jar (对于 org.sqlite.Function) ,这会给应用程序增加不必要的大小。

另一方面,我需要 SQL 中的 Order by 函数,因为单独显示距离并不是什么大问题——我已经在自定义的 SimpleCursorAdapter 中做过了,但是我不能对数据进行排序,因为我的数据库中没有距离列。这意味着每次位置变化时都要更新数据库,这是对电池和性能的浪费。因此,如果有人有任何想法排序光标与数据库中没有列,我也会很感激!

我知道有很多安卓应用程序使用这个功能,但是谁能解释一下它的魔力吗。

顺便说一下,我发现了另一种选择: 在 SQLite 中基于 Radius 获取记录的查询?

它建议为 lat 和 lng 的 cos 和 sin 值创建4个新列,但是还有其他不那么冗余的方法吗?

59401 次浏览

I know this has been answered and accepted but thought I'd add my experiences and solution.

Whilst I was happy to do a haversine function on the device to calculate the accurate distance between the user's current position and any particular target location there was a need to sort and limit the query results in order of distance.

The less than satisfactory solution is to return the lot and sort and filter after the fact but this would result in a second cursor and many unnecessary results being returned and discarded.

My preferred solution was to pass in a sort order of the squared delta values of the long and lats:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))

There's no need to do the full haversine just for a sort order and there's no need to square root the results therefore SQLite can handle the calculation.

EDIT:

This answer is still receiving love. It works fine in most cases but if you need a little more accuracy, please check out the answer by @Teasel below which adds a "fudge" factor that fixes inaccuracies that increase as the latitude approaches 90.

Chris's answer is really useful (thanks!), but will only work if you are using rectilinear coordinates (eg UTM or OS grid references). If using degrees for lat/lng (eg WGS84) then the above only works at the equator. At other latitudes, you need to decrease the impact of longitude on the sort order. (Imagine you're close to the north pole... a degree of latitude is still the same as it is anywhere, but a degree of longitude may only be a few feet. This will mean that the sort order is incorrect).

If you are not at the equator, pre-calculate the fudge-factor, based on your current latitude:

<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);

Then order by:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)

It's still only an approximation, but much better than the first one, so sort order inaccuracies will be much rarer.

1) At first filter your SQLite data with a good approximation and decrease amount of data that you need to evaluate in your java code. Use the following procedure for this purpose:

To have a deterministic threshold and more accurate filter on data, It is better to calculate 4 locations that are in radius meter of the north, west, east and south of your central point in your java code and then check easily by less than and more than SQL operators (>, <) to determine if your points in database are in that rectangle or not.

The method calculateDerivedPosition(...) calculates those points for you (p1, p2, p3, p4 in picture).

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;


}

And now create your query:

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 is the name of the column in the database that stores latitude values and COL_Y is for longitude.

So you have some data that are near your central point with a good approximation.

2) Now you can loop on these filtered data and determine if they are really near your point (in the circle) or not using the following methods:

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

Enjoy!

I used and customized this reference and completed it.

Try something like this:

    //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's id
id = _myCursor.getInt(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_ID));
}
}

After this, id contains the item you want from the database so you can fetch it:

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

Hope that helps!

In order to increase performance as much as possible I suggest improve @Chris Simpson's idea with the following ORDER BY clause:

ORDER BY (<L> - <A> * LAT_COL - <B> * LON_COL + LAT_LON_SQ_SUM)

In this case you should pass the following values from code:

<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
<B> = 2 * center_lon

And you should also store LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2 as additional column in database. Populate it inserting your entities into database. This slightly improves performance while extracting large amount of data.