Pregunta SQlite Obteniendo las 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 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 Haversine (calcular la distancia en una esfera), pero no he encontrado nada escrito en Java y funciona .

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 Ordenar por SQL, porque la visualización de la distancia por sí sola no es un gran problema, ya lo hice en mi CustomCursorAdapter 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 cambie la ubicación y eso es una pérdida de batería y rendimiento. Entonces, si alguien tiene alguna idea sobre ordenar el cursor con una columna que no está en la base de datos, ¡le estaría agradecido también!

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?

Está sugiriendo hacer 4 nuevas columnas para valores cos y sin lat y lng, pero ¿hay alguna otra manera no tan redundante?


73
2017-09-12 14:44


origen


Respuestas:


1) En primer lugar, filtre sus datos SQLite con una buena aproximación y disminuya la cantidad de datos que necesita evaluar en su código java. Use el siguiente procedimiento para este propósito:

Para tener un determinista límite y un filtro más preciso en los datos, es mejor calcular 4 ubicaciones que están en radius metro del norte, oeste, este y sur de su punto central en tu código java y entonces controle fácilmente por menos y más que los operadores de 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 para usted (p1, p2, p3, p4 en la imagen).

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;

    }

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 los valores de latitud y COL_Y es por 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 utilizando 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 lo completó.


100
2017-10-21 12:42



La respuesta de Chris es realmente útil (¡gracias!), Pero solo funcionará si está utilizando coordenadas rectilíneas (por ejemplo, referencias de cuadrícula UTM u OS). Si usa grados para lat / lng (p. Ej., WGS84), lo anterior solo funciona en el ecuador. En otras latitudes, debe disminuir 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 solo puede ser de unos pocos pies. Esto significará que el orden de clasificación es incorrecto).

Si no estás en el ecuador, precalcula el factor de dulce de azúcar, basado en tu 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 imprecisiones de orden serán mucho más raras.


65
2017-09-19 14:06



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

Aunque estaba contento de hacer una función de havers en el dispositivo para calcular la distancia exacta entre la posición actual del usuario y cualquier ubicación de destino en particular, hubo una necesidad de 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 fue pasar en un orden de clasificación de los valores delta al cuadrado del largo y lats:

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

No es necesario hacer el havers completo solo para un orden de clasificación y no hay necesidad de raíz cuadrada de los resultados, por lo tanto 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 "dulce" que corrige las imprecisiones que aumentan a medida que la latitud se acerca a 90.


63
2017-08-31 18:25



¿Has considerado un Geohash etiqueta / índice para sus entradas para reducir el tamaño de su conjunto de resultados y luego aplicar la función apropiada.

Otra pregunta de stackoverflow en un área similar: Encontrar el punto más cercano a un punto dado


2
2017-09-12 18:40



Para aumentar el rendimiento tanto como sea posible, sugiero mejorar la idea de @Chris Simpson con la siguiente ORDER BY clá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 deberías almacenar LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2 como columna adicional en la base de datos. Rellenarlo insertando tus entidades en la base de datos. Esto mejora ligeramente el rendimiento al extraer gran cantidad de datos.


0
2018-05-12 11:37



Eche un vistazo a esta publicación:

Función de distancia para sqlite

Parece que le permite agregar una función Distance () personalizada a SQLite que podría permitirle evitar pasar por todos los aros en las otras respuestas.


-1
2018-01-16 18:20



Pruebe 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!


-2
2018-06-13 13:03