SQlite Obtener ubicaciones más cercanas (con latitud y longitud)
Tengo datos con latitud y longitud almacenados en mi base de datos SQLite y quiero obtener las ubicaciones más cercanas a los parámetros que ingresé (por ejemplo, mi ubicación actual: lat/lng, etc.).
Sé que esto es posible en MySQL, y he investigado bastante que SQLite necesita una función externa personalizada para la fórmula de Haversine (cálculo de distancia en una esfera), pero no he encontrado nada escrito en Java que funcione. .
Además, si quiero agregar funciones personalizadas, necesito el org.sqlite
archivo .jar (para org.sqlite.Function
), y eso agrega un tamaño innecesario a la aplicación.
El otro lado de esto es que necesito la función Ordenar por de SQL, porque mostrar la distancia por sí sola no es un gran problema; ya lo hice en mi SimpleCursorAdapter personalizado, pero no puedo ordenar los datos porque No tengo la columna de distancia en mi base de datos. Eso significaría actualizar la base de datos cada vez que cambia la ubicación y eso es un desperdicio de batería y rendimiento. Entonces, si alguien tiene alguna idea sobre cómo ordenar el cursor con una columna que no está en la base de datos, ¡también se lo agradecería!
Sé que hay toneladas de aplicaciones de Android que utilizan esta función, pero ¿alguien puede explicar la magia?
Por cierto, encontré esta alternativa: ¿Consulta para obtener registros basados en Radius en SQLite?
Se sugiere crear 4 columnas nuevas para los valores cos y sin de lat y lng, pero ¿hay alguna otra forma que no sea tan redundante?
1) Al principio filtre sus datos SQLite con una buena aproximación y disminuya la cantidad de datos que necesita evaluar en su código Java. Utilice el siguiente procedimiento para este propósito:
Para tener un umbral determinista y un filtro de datos más preciso, es mejor calcular 4 ubicaciones que están en radius
metros del norte, oeste, este y sur de su punto central en su código Java y luego verificar fácilmente por menos y más que Operadores SQL (>, <) para determinar si sus puntos en la base de datos están en ese rectángulo o no.
El método calculateDerivedPosition(...)
calcula esos puntos por usted (p1, p2, p3, p4 en la imagen).
/**
* 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;
}
Y ahora crea tu consulta:
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
es el nombre de la columna en la base de datos que almacena valores de latitud y COL_Y
es para longitud.
Entonces tienes algunos datos que están cerca de tu punto central con una buena aproximación.
2) Ahora puedes recorrer estos datos filtrados y determinar si realmente están cerca de tu punto (en el círculo) o no usando los siguientes métodos:
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;
}
¡Disfrutar!
Utilicé y personalicé esta referencia y la completé.
La respuesta de Chris es realmente útil (¡gracias!), pero solo funcionará si utiliza coordenadas rectilíneas (por ejemplo, referencias de cuadrícula UTM o OS). Si usa grados para lat/lng (por ejemplo, WGS84), lo anterior solo funciona en el ecuador. En otras latitudes, es necesario reducir el impacto de la longitud en el orden de clasificación. (Imagínese que está cerca del polo norte... un grado de latitud sigue siendo el mismo que en cualquier otro lugar, pero un grado de longitud puede ser sólo de unos pocos pies. Esto significará que el orden de clasificación es incorrecto).
Si no estás en el ecuador, calcula previamente el factor de manipulación, según tu latitud actual:
<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);
Entonces ordena por:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)
Sigue siendo sólo una aproximación, pero mucho mejor que la primera, por lo que las imprecisiones en el orden de clasificación serán mucho más raras.
Sé que esto ha sido respondido y aceptado, pero pensé en agregar mis experiencias y mi solución.
Si bien estaba feliz de realizar una función haversine en el dispositivo para calcular la distancia exacta entre la posición actual del usuario y cualquier ubicación objetivo en particular, era necesario ordenar y limitar los resultados de la consulta en orden de distancia.
La solución menos que satisfactoria es devolver el lote y ordenar y filtrar después del hecho, pero esto daría como resultado un segundo cursor y muchos resultados innecesarios que se devolverían y descartarían.
Mi solución preferida era pasar en orden los valores delta cuadrados de los largos y los dorsales:
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
(<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))
No es necesario realizar el análisis completo solo para ordenar y no es necesario aplicar la raíz cuadrada a los resultados, por lo que SQLite puede manejar el cálculo.
EDITAR:
Esta respuesta todavía está recibiendo amor. Funciona bien en la mayoría de los casos, pero si necesita un poco más de precisión, consulte la respuesta de @Teasel a continuación, que agrega un factor "fudge" que corrige las imprecisiones que aumentan a medida que la latitud se acerca a 90.