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

Posted

技术标签:

【中文标题】SQlite 获取最近的位置(经纬度)【英文标题】:SQlite Getting nearest locations (with latitude and longitude) 【发布时间】:2010-09-12 14:44:58 【问题描述】:

我的 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 是否适合您(即使公式不正确)? 不,我找到了一个(冗余)替代方案(编辑后的帖子),听起来比在应用程序中添加 2.6MB .jar 更好。但我仍在寻找更好的解决方案。谢谢! 返回距离单位类型是什么? 这是一个full implementation,用于在 Android 上根据您的位置与对象位置之间的距离构建 SQlite 查询。 【参考方案1】:

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

为了对数据有一个确定性的阈值和更准确的过滤器,最好计算在radius北、西、东米内的4个位置并在您的中心点以南在您的 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;
    

享受吧!

我使用并定制了this reference并完成了它。

【讨论】:

如果您正在寻找上述概念的 javascript 实现,请查看 Chris Veness 的精彩网页。 movable-type.co.uk/scripts/latlong.html @Menma x 是纬度,y 是经度。 radius:图中圆的半径。 上面给出的解决方案是正确的并且有效。试试看... :) 这是一个近似解决方案!它通过快速、索引友好的 SQL 查询为您提供高度近似的结果。在某些极端情况下会给出错误的结果。在几公里范围内获得少量近似结果后,使用更慢、更精确的方法过滤这些结果。不要使用它来过滤半径非常大的过滤器,或者如果您的应用程序将在赤道经常使用! 但我不明白。 CalculateDerivedPosition 正在将 lat,lng 坐标转换为笛卡尔坐标,然后您在 SQL 查询中将这些笛卡尔值与 lat,long 值进行比较。两个不同的几何坐标?这是如何运作的?谢谢。【参考方案2】:

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

这仍然只是一个近似值,但比第一个要好得多,因此排序顺序不准确的情况会少得多。

【讨论】:

这是一个非常有趣的观点,即纵向线在两极处会聚,并且距离越近,结果就会出现偏差。很好的修复。 这似乎工作 cursor = 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); 应该((&lt;lat&gt; - LAT_COLUMN) * (&lt;lat&gt; - LAT_COLUMN) + (&lt;lng&gt; - LNG_COLUMN) * (&lt;lng&gt; - LNG_COLUMN) * &lt;fudge&gt;) 小于distancedistance^2 软糖因子不是以弧度为单位,列不是以度为单位吗?不应该换成同一个单位吗? 不,软糖因子是一个比例因子,在两极为 0,在赤道为 1。它既不是度数,也不是弧度,它只是一个无单位的数字。 Java Math.cos 函数需要以弧度表示的参数,我假设 以度为单位,因此 Math.toRadians 函数。但是得到的余弦没有单位。【参考方案3】:

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

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

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

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

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

无需为排序顺序执行完整的半正弦运算,也无需对结果进行平方根,因此 SQLite 可以处理计算。

编辑:

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

【讨论】:

很好的答案。你能解释一下它是如何工作的吗?这个算法叫什么? @iMatoria - 这只是毕达哥拉斯著名定理的缩减版。给定两组坐标,两个 X 值之间的差异表示直角三角形的一侧,而 Y 值之间的差异表示另一侧。要获得斜边(以及因此点之间的距离),您可以将这两个值的平方相加,然后将结果平方根。在我们的例子中,我们不做最后一点(平方根),因为我们做不到。幸运的是,这对于排序顺序不是必需的。 在我的应用程序 BostonBusMap 中,我使用了这个解决方案来显示离当前位置最近的站点。但是,您需要将经度距离缩放cos(latitude) 以使纬度和经度大致相等。见en.wikipedia.org/wiki/…【参考方案4】:

为了尽可能提高性能,我建议使用以下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 存储为数据库中的附加列。填充它,将您的实体插入数据库。这在提取大量数据的同时略微提高了性能。

【讨论】:

【参考方案5】:

试试这样的:

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

希望有帮助!

【讨论】:

以上是关于SQlite 获取最近的位置(经纬度)的主要内容,如果未能解决你的问题,请参考以下文章

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

如何从纬度/经度获取附近的位置?

根据经纬度获取最近的位置

根据地理位置来获取最近的商家

sql 使用最近的纬度和经度坐标(MySQL和SQLite)进行SQL排序

如何从当前位置找到最近的经纬度?