我的SQLite数据库中存储了纬度和经度的数据,我希望得到最接近我输入的参数的位置(例如我的当前位置 - lat / lng等)。
我知道在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个新列,但是还有其他的,不那么冗余的方式吗?
答案 0 :(得分:104)
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并完成了它。
答案 1 :(得分:67)
Chris的答案非常有用(谢谢!),但只有在使用直线坐标(例如UTM或OS网格参考)时才会起作用。如果使用lat / lng的度数(例如WGS84),则上述仅适用于赤道。在其他纬度,您需要减少经度对排序顺序的影响。 (想象一下,你接近北极......纬度仍然与任何地方相同,但经度可能只有几英尺。这意味着排序顺序不正确。) / p>
如果您不在赤道,请根据您当前的纬度预先计算软糖因子:
<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);
然后按顺序排序:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)
它仍然只是一个近似值,但比第一个好得多,因此排序顺序的不准确性将会非常罕见。
答案 2 :(得分:65)
我知道这已得到回答和接受,但我认为我会添加我的经验和解决方案。
虽然我很高兴在设备上执行半正功能来计算用户当前位置与任何特定目标位置之间的准确距离,但仍需要按距离的顺序对查询结果进行排序和限制。
不太令人满意的解决方案是返回批次并在事后进行排序和过滤,但这会导致第二个游标并返回并丢弃许多不必要的结果。
我首选的解决方案是传递long和lats的平方delta值的排序顺序:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))
没有必要仅为排序顺序执行完整的半正式操作,因此无需将结果平方根,因此SQLite可以处理计算。
修改强>
这个答案仍在接受爱。它在大多数情况下都能正常工作,但是如果你需要更高的准确度,请查看@Teasel下面的答案,它增加了一个“软糖”因子,可以解决随着纬度接近90而增加的不准确性。
答案 3 :(得分:2)
您是否考虑过条目的Geohash标记/索引以减小结果集的大小,然后应用相应的函数。
类似区域中的另一个stackoverflow问题: finding-the-closest-point-to-a-given-point
答案 4 :(得分: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
存储为数据库中的附加列。填充它将您的实体插入数据库。这在提取大量数据的同时略微提高了性能。
答案 5 :(得分:-1)
答案 6 :(得分:-2)
尝试这样的事情:
//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));
希望有所帮助!