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


86

我在SQLite数据库中存储了经度和纬度数据,我想获取与所输入参数最接近的位置(例如,我当前的位置-纬度/经度等)。

我知道这在MySQL中是可能的,并且我已经做了大量的研究,认为SQLite需要Haversine公式的自定义外部函数(计算球体上的距离),但是我还没有找到任何用Java编写并且可以工作的东西。

另外,如果要添加自定义功能,则需要org.sqlite.jar(用于org.sqlite.Function),这会为应用添加不必要的尺寸。

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

我知道目前有大量使用此功能的Android应用程序,但是有人可以解释它的神奇之处。

顺便说一下,我找到了这种选择:在SQLite中查询以获取基于Radius的记录?

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


您是否检查过org.sqlite.Function是否适合您(即使公式不正确)?
Thomas Mueller 2010年

不,我发现(冗余)替代方法(编辑过的帖子)听起来比在应用程序中添加2,6MB .jar更好。但我仍在寻找更好的解决方案。谢谢!
朱尔2010年

返回距离单位类型是什么?

这是一个完整的实现,用于根据您的位置与对象位置之间的距离在Android上构建SQlite查询。
EricLarch

Answers:


112

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

为了具有确定性的阈值并更准确地过滤数据,最好在Java代码中计算以中心点的北,西,东和南为单位的4个位置,然后轻松检查小于和等于SQL运算符(>,<)确定数据库中的点是否在该矩形中。radius

该方法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;
    }

请享用!

我使用并定制了此参考资料并完成了它。


如果您正在上面寻找这个概念的Javascript实现,请查看Chris Veness出色的网页。moving-type.co.uk/scripts/latlong.html
barneymc 2014年

@Menma x是纬度,y是经度。radius:图片中显示的圆的半径。
鲍勃2015年

上面给出的解决方案是正确的并且可行。尝试... :)
YS 2015年

1
这是一个近似的解决方案!通过快速的,索引友好的SQL查询,可以为您提供高度近似的结果。在某些极端情况下,它将给出错误的结果。在几公里范围内获得少量近似结果后,请使用更慢,更精确的方法来过滤这些结果。如果您的应用将在赤道上经常使用,请不要使用它来过滤半径很大的物体!
user1643723

1
但是我不明白。CalculateDerivedPosition将lat,lng坐标转换为笛卡尔坐标,然后在SQL查询中将这些笛卡尔值与lat,long值进行比较。两种不同的几何坐标?这是如何运作的?谢谢。
误解

70

克里斯的答案确实很有用(谢谢!),但仅在使用直线坐标(例如UTM或OS网格参考)时才有效。如果对纬度/经度使用度数(例如WGS84),则以上仅适用于赤道。在其他纬度上,您需要减少经度对排序顺序的影响。(假设您靠近北极……纬度与任何地方都一样,但是经度可能只有几英尺。这意味着排序顺序是错误的)。

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

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

然后按以下顺序订购:

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

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


3
关于纵向线在极点处汇合并且使结果越接近越偏斜,这是一个非常有趣的观点。好的修复。
克里斯·辛普森

1
这似乎可以工作游标= db.getReadableDatabase()。rawQuery(“选择nome,id作为_id,” +“(”“纬度+”-纬度)*(“ +纬度+”-纬度)+(“ +经度+ “-lon)*(” +经度+“-lon)*” +软糖+“ as distanza” +“来自客户” +“由distanza asc排序”,null);
max4ever

应该((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)小于distancedistance^2
鲍勃2012年

软糖因子不是弧度,列不是度数吗?他们不应该转换成相同的单位吗?
Rangel Reale

1
不,软糖系数是一个比例系数,在极点处为0,在赤道处为1。它既不是度数也不是弧度,而是一个无单位的数字。Java Math.cos函数需要一个以弧度为单位的参数,我假设<lat>以度为单位,因此是Math.toRadians函数。但是生成的余弦没有单位。
Teasel

68

我知道这已经得到答复和接受,但以为我会补充我的经验和解决方案。

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

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

我的首选解决方案是按long和lat的平方增量值的排序顺序进行传递:

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

无需仅按照排序顺序就可以使用完整的haversine,也不必对结果求平方,因此SQLite可以处理计算。

编辑:

这个答案仍在接受爱。在大多数情况下,它可以正常工作,但是如果您需要更高的精度,请在下面的@Teasel中查看答案,其中添加了一个“错误”因素,以修正随着纬度接近90而增加的不准确性。


好答案。您能解释一下它是如何工作的,该算法称为什么?
iMatoria

3
@iMatoria-这只是毕达哥拉斯著名定理的简化版本。给定两组坐标,两个X值之间的差代表直角三角形的一侧,而Y值之间的差则代表另一侧。要获得斜边(以及点之间的距离),请将这两个值的平方相加,然后对结果求平方。在我们的例子中,我们不做最后一点(平方根),因为我们做不到。幸运的是,这对于排序顺序不是必需的。
克里斯·辛普森

6
在我的应用程序BostonBusMap中,我使用了此解决方案来显示最接近当前位置的站点。但是,您需要按比例缩放经度距离,cos(latitude)以使纬度和经度大致相等。见en.wikipedia.org/wiki/...
noisecapella

0

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

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作为附加列存储在数据库中。填充它,将您的实体插入数据库。在提取大量数据的同时,这会稍微提高性能。


-3

尝试这样的事情:

    //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包含您想要从数据库中获取的项目,因此您可以获取它:

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

希望有帮助!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.