SQlite Obtener ubicaciones más cercanas (con latitud y longitud)

86

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 puse (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 (calcular la distancia en una esfera), pero no he encontrado nada que esté escrito en Java y funcione. .

Además, si quiero agregar funciones personalizadas, necesito el org.sqlite.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 Order by 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 una pérdida 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, ¡yo también estaría agradecido!

Sé que hay toneladas de aplicaciones de Android que usan 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 no tan redundante?

Jure
fuente
¿Verificó si org.sqlite.Function funciona para usted (incluso si la fórmula no es correcta)?
Thomas Mueller
No, encontré una alternativa (redundante) (publicación editada) que suena mejor que agregar un .jar de 2,6 MB en la aplicación. Pero sigo buscando una mejor solución. ¡Gracias!
Jure
¿Cuál es el tipo de unidad de distancia de retorno?
Aquí hay una implementación completa para crear una consulta SQlite en Android basada en la distancia entre su ubicación y la ubicación del objeto.
EricLarch

Respuestas:

112

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 más preciso en los datos, es mejor calcular 4 ubicaciones que están en radiusmetros del norte, oeste, este y sur de su punto central en su código java y luego verificar fácilmente por menos que 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).

ingrese la descripción de la imagen aquí

/**
* 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_Xes el nombre de la columna de la base de datos que almacena los valores de latitud y COL_Ycorresponde a la longitud.

Entonces tiene algunos datos que están cerca de su punto central con una buena aproximación.

2) Ahora puede recorrer estos datos filtrados y determinar si están realmente cerca de su 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!

Usé y personalicé esta referencia y la completé.

Bobs
fuente
Consulte la excelente página web de Chris Veness si está buscando una implementación de Javascript de este concepto anterior. movable-type.co.uk/scripts/latlong.html
barneymc
@Menma x es latitud e y es longitud. radio: el radio del círculo que se muestra en la imagen.
Bobs
la solución dada arriba es correcta y funciona. Pruébelo ... :)
YS
1
¡Esta es una solución aproximada! Le brinda resultados muy aproximados a través de una consulta SQL rápida y compatible con índices. Dará un resultado incorrecto en algunas circunstancias extremas. Una vez que haya obtenido una pequeña cantidad de resultados aproximados en un área de varios kilómetros, use métodos más lentos y precisos para filtrar esos resultados . ¡No lo use para filtrar con un radio muy grande o si su aplicación se va a usar con frecuencia en el ecuador!
user1643723
1
Pero no lo entiendo. CalculateDerivedPosition está transformando la coordenada lat, lng en una cartesiana y luego en la consulta SQL, está comparando estos valores cartesianos con los valores lat, long. ¿Dos coordenadas geométricas diferentes? ¿Como funciona esto? Gracias.
Misgevolution
70

La respuesta de Chris es realmente útil (¡gracias!), Pero solo funcionará si está utilizando 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. (Imagina que estás cerca del polo norte ... un grado de latitud sigue siendo el mismo que en cualquier lugar, pero un grado de longitud puede ser solo unos pocos pies. Esto significa que el orden de clasificación es incorrecto).

Si no se encuentra en el ecuador, calcule previamente el factor fudge, según su latitud actual:

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

Luego ordene por:

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

Sigue siendo solo una aproximación, pero mucho mejor que la primera, por lo que las inexactitudes en el orden de clasificación serán mucho más raras.

Carda
fuente
3
Ese es un punto realmente interesante sobre las líneas longitudinales que convergen en los polos y sesgan los resultados cuanto más se acercan. Buen arreglo.
Chris Simpson
1
esto parece funcionar cursor = db.getReadableDatabase (). rawQuery ("Seleccionar nombre, id como _id," + "(" + latitud + "- lat) * (" + latitud + "- lat) + (" + longitud + "- lon) * (" + longitud + "- lon) *" + fudge + "as distanza" + "de cliente" + "orden por distanza asc", nulo);
max4ever
debe ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)ser menor que distanceo distance^2?
Bobs
¿No está el factor fudge en radianes y las columnas en grados? ¿No deberían convertirse a la misma unidad?
Rangel Reale
1
No, el factor fudge es un factor de escala que es 0 en los polos y 1 en el ecuador. No está en grados ni radianes, es solo un número sin unidades. La función Java Math.cos requiere un argumento en radianes, y supuse que <lat> estaba en grados, de ahí la función Math.toRadians. Pero el coseno resultante no tiene unidades.
Teasel
68

Sé que esto ha sido respondido y aceptado, pero pensé en agregar mis experiencias y solución.

Si bien estaba feliz de hacer una función haversine en el dispositivo para calcular la distancia precisa 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 devueltos y descartados.

Mi solución preferida fue pasar en un orden de clasificación de los valores delta al cuadrado de long y lats:

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

No es necesario hacer el análisis completo solo para un orden de clasificación y no es necesario enraizar los resultados en raíz cuadrada, por lo que SQLite puede manejar el cálculo.

EDITAR:

Esta respuesta sigue 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.

Chris Simpson
fuente
Gran respuesta. ¿Podría explicar cómo funcionó y cómo se llama este algoritmo?
iMatoria
3
@iMatoria - Esta es solo una versión reducida del famoso teorema de Pitágoras. Dados dos conjuntos de coordenadas, la diferencia entre los dos valores X representa un lado de un triángulo rectángulo y la diferencia entre los valores Y es el otro. Para obtener la hipotenusa (y por lo tanto la distancia entre los puntos), suma los cuadrados de estos dos valores y luego cuadra el resultado. En nuestro caso no hacemos el último bit (el enraizamiento cuadrado) porque no podemos. Afortunadamente, esto no es necesario para un orden de clasificación.
Chris Simpson
6
En mi aplicación BostonBusMap utilicé esta solución para mostrar las paradas más cercanas a la ubicación actual. Sin embargo, debe escalar la distancia de longitud cos(latitude)para que la latitud y la longitud sean aproximadamente iguales. Ver en.wikipedia.org/wiki/…
noisecapella
0

Para aumentar el rendimiento tanto como sea posible, sugiero mejorar la idea de @Chris Simpson con la siguiente ORDER BYcláusula:

ORDER BY (<L> - <A> * LAT_COL - <B> * LON_COL + LAT_LON_SQ_SUM)

En este caso, debe pasar los siguientes valores del código:

<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
<B> = 2 * center_lon

Y también debe almacenar LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2como columna adicional en la base de datos. Rellénelo insertando sus entidades en la base de datos. Esto mejora ligeramente el rendimiento al extraer una gran cantidad de datos.

Sergey Metlov
fuente
-3

Prueba algo como esto:

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

Después de esto, id contiene el elemento que desea de la base de datos para que pueda recuperarlo:

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

¡Espero que ayude!

Scott Helme
fuente