获取最接近的位置(带有纬度和经度)的SQlite方法

91
我在SQLite数据库中存储了纬度和经度数据,想要获取距离我输入的参数(例如:我的当前位置-纬度/经度等)最近的地点。 我知道MySQL可以做到这一点。我已经做了相当多的研究,发现SQLite需要一个用于Haversine公式(用于计算球体上的距离)的自定义外部函数,但我没有找到任何用Java编写并可行的函数。 另外,如果我想要添加自定义函数,我需要使用org.sqlite.jar(用于org.sqlite.Function),这会增加应用程序的不必要的大小。 此外,我还需要SQL中的Order by函数,因为仅显示距离并不是问题,我已经在自定义SimpleCursorAdapter中完成了它,但无法对数据进行排序,因为在我的数据库中没有距离列。这意味着每次位置变化时都需要更新数据库,这会浪费电池和性能。因此,如果有人有任何关于如何使用不在数据库中的列对光标进行排序的想法,我将非常感激! 我知道有许多Android应用程序使用此功能,但有人可以解释一下其中的魔法吗? 顺便说一下,我找到了这种替代方法:Query to get records based on Radius in SQLite?,它建议为lat和lng的cos和sin值创建4个新列,但是否有其他不那么冗余的方法?

你有检查过 org.sqlite.Function 是否适用于你(即使公式不正确)吗? - Thomas Mueller
不,我找到了一个(冗余的)替代方案(编辑过的帖子),听起来比在应用程序中添加2.6MB的.jar文件更好。但我仍在寻找更好的解决方案。谢谢! - Jure
返回的距离单位类型是什么? - user959001
这里有一个完整的实现,用于在Android上构建基于您的位置和对象位置之间距离的SQlite查询:https://dev59.com/_3A75IYBdhLWcg3wv73A#9536914 - EricLarch
5个回答

115

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

为了在数据上有一个确定性的阈值和更精确的过滤器,最好在Java代码中计算4个位置,这四个位置分别在中心点半径米以北、西、东、南的位置,然后使用比较运算符(<, >)轻松地判断数据库中的点是否在该矩形内。

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

享受吧!

我使用并自定义了这个参考资料,并完成了它。


如果你正在寻找一个JavaScript实现上述概念的话,可以查看Chris Veness的网站:http://www.movable-type.co.uk/scripts/latlong.html - barneymc
@Menma x 是纬度,y 是经度。半径:图片中显示的圆的半径。 - Bobs
以上给出的解决方案是正确的并且有效。尝试一下吧... :) - Yash Sampat
1
这是一个近似解决方案!它通过快速、索引友好的SQL查询为您提供高度近似的结果。在某些极端情况下,它会给出错误的结果。一旦您在几公里范围内获得了少量的近似结果,请使用更慢、更精确的方法来过滤这些结果。不要在非常大的半径范围内使用它进行过滤,或者如果您的应用程序将经常在赤道使用! - user1643723
1
但我不明白。CalculateDerivedPosition正在将lat,lng坐标转换为笛卡尔坐标系,然后在SQL查询中,您正在将这些笛卡尔值与lat,long值进行比较。两个不同的几何坐标?这是如何工作的?谢谢。 - Misgevolution
显示剩余6条评论

74

Chris的回答非常有用(谢谢!),但仅适用于使用直角坐标系(例如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
关于经线在极点汇聚并导致越靠近时结果偏斜这一点,真的很有意思。不错的解决方案。 - Chris Simpson
1
似乎这个有效。游标 = db.getReadableDatabase().rawQuery("Select nome, id as _id, " + "( " + latitude + " - lat) * ( " + latitude +"- lat) + ( " + longitude + "- lon) * ( " + longitude + "- lon) * " + fudge + " as distanza " + " from cliente "+ " order by distanza asc", null); - max4ever
应该是 ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>) 是否小于 distancedistance^2 - Bobs
1
不,调整因子是一个缩放因子,在极点为0,在赤道为1。它既不是以度数表示,也不是弧度,只是一个无单位的数字。Java Math.cos函数需要一个弧度参数,我假设<lat>是以度数表示的,因此使用了Math.toRadians函数。但是得到的余弦值没有单位。 - Teasel
这只是 SQL 中的“order by”子句... 它确保最接近的记录位于顶部,但不计算距离。因此,一旦您获得了结果集,您应该在代码中使用 Haversine 公式计算距离。例如,您可以使用 breceivemail 的 getDistanceBetweenTwoPoints 函数,如上所述。该函数硬编码了地球半径为 6371000,因此返回的距离将以米为单位。如果您想要不同单位的结果,请更改该数字(例如,使用 31670 来表示弗隆)。 - Teasel
显示剩余2条评论

73

虽然这个问题已经被回答并且已经得到认可,但是我想分享一下我的经验和解决方法。

虽然我很乐意在设备上执行haversine函数来计算用户当前位置与任何特定目标位置之间的精确距离,但有时需要按照距离排序和限制查询结果。

一个不太令人满意的解决方案是返回所有结果,并在此之后进行排序和过滤,但这将导致第二个光标和许多不必要的结果被返回和丢弃。

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

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

为了排序而进行完整的haversine计算是没有必要的,也没有必要对结果进行平方根,因此SQLite可以处理这个计算。

编辑:

这个答案仍然受到关注。在大多数情况下它都能正常工作,但如果你需要更高的精度,请查看@Teasel的回答,他添加了一个“调整系数”,可以修复随着纬度接近90度而增加的不准确性。


很棒的答案。你能解释一下它是如何工作的,这个算法叫什么吗? - iMatoria
3
这只是毕达哥拉斯著名定理的简化版。给定两组坐标,它们的X值之差代表一个直角三角形的一条边,它们的Y值之差则代表另一条边。要得到斜边(因而得到点之间的距离),你需要将这两个数值的平方相加,然后对结果开平方根。在我们的情况下,我们不进行最后一步(即开平方根),因为我们不能这么做。幸运的是,这对于排序顺序并不必要。 - Chris Simpson
6
在我的应用程序BostonBusMap中,我使用了这种解决方案来显示最靠近当前位置的站点。然而,您需要通过cos(latitude)来缩放经度距离,以使纬度和经度大致相等。请参阅http://en.wikipedia.org/wiki/Geographical_distance#Spherical_Earth_projected_to_a_plane。 - 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)); 

希望这能帮到你!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接