关于java:SQlite获取最近的位置(纬度和经度)

SQlite Getting nearest locations (with latitude and longitude)

我的SQLite数据库中存储了纬度和经度数据,我希望得到最接近我输入参数的位置(例如我的当前位置 - lat / lng等)。

我知道这在MySQL中是可能的,我已经做了很多研究,SQLite需要为Haversine公式定义外部函数(计算球体上的距离),但我没有发现任何用Java编写的工作。

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

另一方面,我需要SQL中的Order by函数,因为单独显示距离并不是一个大问题 - 我已经在我的自定义SimpleCursorAdapter中做了,但是我无法对数据进行排序,因为我我的数据库中没有距离列。这意味着每次更改位置时都会更新数据库,这会浪费电池和性能。因此,如果有人知道使用不在数据库中的列对游标进行排序,我将不胜感激!

我知道有很多Android应用程序使用这个功能,但有人可以解释一下魔术。

顺便说一句,我找到了另一种方法:查询在SQLite中获取基于Radius的记录?

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


1)首先使用良好的近似值过滤SQLite数据,并减少需要在java代码中评估的数据量。为此,请使用以下过程:

要获得确定性阈值并对数据进行更准确的过滤,最好在java代码中计算中心点北,西,东,南的radius米中的4个位置,然后通过小于并且不仅仅是SQL运算符(>,<)来确定数据库中的点是否在该矩形中。

方法calculateDerivedPosition(...)为你计算这些点(图中的p1,p2,p3,p4)。

enter image description here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 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;

    }

现在创建您的查询:

1
2
3
4
5
6
7
8
9
10
11
12
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)现在,您可以循环使用这些过滤后的数据,并使用以下方法确定它们是否真正靠近您的点(在圆圈中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
    }

请享用!

我使用并定制了这个参考并完成了它。


Chris的答案非常有用(谢谢!),但只有在使用直线坐标(例如UTM或OS网格参考)时才能使用。如果使用lat / lng的度数(例如WGS84),则上述仅适用于赤道。在其他纬度,您需要减少经度对排序顺序的影响。 (想象一下,你接近北极......纬度仍然与任何地方相同,但经度可能只有几英尺。这意味着排序顺序不正确)。

如果您不在赤道,请根据您当前的纬度预先计算软糖因子:

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

然后订购:

(( - LAT_COLUMN) * ( - LAT_COLUMN) +
( - LNG_COLUMN) * ( - LNG_COLUMN) * )

它仍然只是一个近似值,但比第一个好得多,因此排序顺序的不准确性将更加罕见。


我知道这已经得到了回答和接受,但我想我会添加我的经验和解决方案。

虽然我很高兴在设备上执行半正功能来计算用户当前位置与任何特定目标位置之间的准确距离,但仍需要按距离的顺序对查询结果进行排序和限制。

不太令人满意的解决方案是返回批次并在事后进行排序和过滤,但这会导致第二个游标并返回并丢弃许多不必要的结果。

我首选的解决方案是传递long和lats的平方delta值的排序顺序:

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

没有必要只为排序顺序执行完整的hasrsine,并且不需要将结果平方根,因此SQLite可以处理计算。

编辑:

这个答案仍在接受爱。它在大多数情况下都能正常工作,但是如果你需要更高的准确度,请查看@Teasel下面的答案,它增加了一个"软糖"因子,可以解决随着纬度接近90而增加的不准确性。


您是否考虑过输入的Geohash标记/索引以减小结果集的大小,然后应用相应的函数。

另一个类似区域的stackoverflow问题:
寻找最最接近的点到A-给点


为了尽可能提高性能,我建议使用以下ORDER BY子句改进@Chris Simpson的想法:

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

在这种情况下,您应该从代码中传递以下值:

1
2
3
<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
 = 2 * center_lon

您还应该将LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2存储为数据库中的附加列。 填充它将您的实体插入数据库。 这在提取大量数据的同时略微提高了性能。


看看这篇文章:

sqlite的距离函数

它似乎允许您向SQLite添加自定义Distance()函数,这可能允许您避免跳过其他答案中的所有箍。


尝试这样的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    //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));
        }
    }

在此之后,id包含您希望从数据库中获取的项目,以便您可以获取它:

1
2
3
4
5
6
7
    //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));

希望有所帮助!