SQlite Obtenir les emplacements les plus proches (avec latitude et longitude)

86

J'ai des données avec la latitude et la longitude stockées dans ma base de données SQLite, et je veux obtenir les emplacements les plus proches des paramètres que j'ai mis (par exemple, ma position actuelle - lat / lng, etc.).

Je sais que cela est possible dans MySQL, et j'ai fait pas mal de recherches sur le fait que SQLite a besoin d'une fonction externe personnalisée pour la formule Haversine (calcul de la distance sur une sphère), mais je n'ai rien trouvé qui soit écrit en Java et fonctionne .

De plus, si je veux ajouter des fonctions personnalisées, j'ai besoin du org.sqlite.jar (pour org.sqlite.Function), et cela ajoute une taille inutile à l'application.

L'autre aspect de ceci est que j'ai besoin de la fonction Order by de SQL, car l'affichage de la distance seule n'est pas vraiment un problème - je l'ai déjà fait dans mon SimpleCursorAdapter personnalisé, mais je ne peux pas trier les données, car je je n'ai pas la colonne de distance dans ma base de données. Cela signifierait mettre à jour la base de données chaque fois que l'emplacement change et c'est un gaspillage de batterie et de performances. Donc, si quelqu'un a une idée sur le tri du curseur avec une colonne qui n'est pas dans la base de données, je lui en serais également reconnaissant!

Je sais qu'il existe des tonnes d'applications Android qui utilisent cette fonction, mais quelqu'un peut-il expliquer la magie.

Au fait, j'ai trouvé cette alternative: une requête pour obtenir des enregistrements basés sur Radius dans SQLite?

Cela suggère de créer 4 nouvelles colonnes pour les valeurs cos et sin de lat et lng, mais y a-t-il un autre moyen pas si redondant?

Jure
la source
Avez-vous vérifié si org.sqlite.Function fonctionne pour vous (même si la formule n'est pas correcte)?
Thomas Mueller
Non, j'ai trouvé une alternative (redondante) (publication modifiée) qui sonne mieux que l'ajout d'un .jar de 2,6 Mo dans l'application. Mais je cherche toujours une meilleure solution. Merci!
Jure
Quel est le type d'unité de distance de retour?
Voici une implémentation complète pour créer une requête SQlite sur Android en fonction de la distance entre votre emplacement et l'emplacement de l'objet.
EricLarch

Réponses:

112

1) Dans un premier temps, filtrez vos données SQLite avec une bonne approximation et réduisez la quantité de données que vous devez évaluer dans votre code java. Utilisez la procédure suivante à cet effet:

Pour avoir un seuil déterministe et un filtre plus précis sur les données, il vaut mieux calculer 4 localisations qui sont en radiusmètre du nord, ouest, est et sud de votre point central dans votre code java puis vérifier facilement par moins de et plus de Opérateurs SQL (>, <) pour déterminer si vos points dans la base de données sont dans ce rectangle ou non.

La méthode calculateDerivedPosition(...)calcule ces points pour vous (p1, p2, p3, p4 sur l'image).

entrez la description de l'image ici

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

    }

Et maintenant créez votre requête:

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_Xest le nom de la colonne de la base de données qui stocke les valeurs de latitude et COL_Ycorrespond à la longitude.

Vous avez donc des données proches de votre point central avec une bonne approximation.

2) Vous pouvez maintenant effectuer une boucle sur ces données filtrées et déterminer si elles sont vraiment proches de votre point (dans le cercle) ou non en utilisant les méthodes suivantes:

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

Prendre plaisir!

J'ai utilisé et personnalisé cette référence et l' ai complétée.

Bobs
la source
Consultez la superbe page Web de Chris Veness si vous recherchez une implémentation Javascript de ce concept ci-dessus. movable-type.co.uk/scripts/latlong.html
barneymc
@Menma x est la latitude et y est la longitude. rayon: le rayon du cercle affiché sur l'image.
Bobs
la solution donnée ci-dessus est correcte et fonctionne. Essayez-le ... :)
YS
1
C'est une solution approximative! Il vous donne des résultats très approximatifs via une requête SQL rapide et conviviale. Cela donnera un résultat erroné dans certaines circonstances extrêmes. Une fois que vous avez obtenu un petit nombre de résultats approximatifs dans une zone de plusieurs kilomètres, utilisez des méthodes plus lentes et plus précises pour filtrer ces résultats . Ne l'utilisez pas pour filtrer avec un très grand rayon ou si votre application va être fréquemment utilisée à l'équateur!
user1643723
1
Mais je ne comprends pas. CalculateDerivedPosition transforme les coordonnées lat, lng en coordonnées cartésiennes, puis dans la requête SQL, vous comparez ces valeurs cartésiennes avec les valeurs lat, longues. Deux coordonnées géométriques différentes? Comment cela marche-t-il? Merci.
Misgevolution
70

La réponse de Chris est vraiment utile (merci!), Mais ne fonctionnera que si vous utilisez des coordonnées rectilignes (par exemple, des références de grille UTM ou OS). Si vous utilisez des degrés pour lat / lng (par exemple WGS84), alors ce qui précède ne fonctionne qu'à l'équateur. À d'autres latitudes, vous devez réduire l'impact de la longitude sur l'ordre de tri. (Imaginez que vous êtes près du pôle nord ... un degré de latitude est toujours le même que n'importe où, mais un degré de longitude ne peut être que de quelques pieds. Cela signifie que l'ordre de tri est incorrect).

Si vous n'êtes pas à l'équateur, pré-calculez le facteur de fudge, en fonction de votre latitude actuelle:

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

Puis commandez par:

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

Ce n'est encore qu'une approximation, mais bien meilleure que la première, donc les inexactitudes d'ordre de tri seront beaucoup plus rares.

Cardère
la source
3
C'est un point vraiment intéressant sur les lignes longitudinales convergeant aux pôles et faussant les résultats à mesure que vous vous approchez. Bonne solution.
Chris Simpson
1
cela semble fonctionner. "- lon) * (" + longitude + "- lon) *" + fudge + "as distanza" + "from cliente" + "order by distanza asc", null);
max4ever
devrait ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)être inférieur à distanceou distance^2?
Bobs
Le facteur de fudge n'est-il pas en radians et les colonnes en degrés? Ne devraient-ils pas être convertis dans la même unité?
Rangel Reale
1
Non, le facteur de fudge est un facteur d'échelle qui est égal à 0 aux pôles et à 1 à l'équateur. Ce n'est ni en degrés, ni en radians, c'est juste un nombre sans unité. La fonction Java Math.cos nécessite un argument en radians, et j'ai supposé que <lat> était en degrés, d'où la fonction Math.toRadians. Mais le cosinus résultant n'a pas d'unités.
Teasel
68

Je sais que cela a été répondu et accepté, mais j'ai pensé ajouter mes expériences et ma solution.

Alors que j'étais heureux de faire une fonction haversine sur l'appareil pour calculer la distance précise entre la position actuelle de l'utilisateur et un emplacement cible particulier, il était nécessaire de trier et de limiter les résultats de la requête par ordre de distance.

La solution moins que satisfaisante est de retourner le lot et de trier et filtrer après coup, mais cela entraînerait un deuxième curseur et de nombreux résultats inutiles renvoyés et rejetés.

Ma solution préférée était de passer dans un ordre de tri des valeurs delta au carré du long et du lats:

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

Il n'est pas nécessaire de faire le haversine complet juste pour un ordre de tri et il n'est pas nécessaire de racine carrée des résultats, SQLite peut donc gérer le calcul.

ÉDITER:

Cette réponse reçoit toujours de l'amour. Cela fonctionne bien dans la plupart des cas, mais si vous avez besoin d'un peu plus de précision, veuillez consulter la réponse de @Teasel ci-dessous qui ajoute un facteur "fudge" qui corrige les inexactitudes qui augmentent à mesure que la latitude approche 90.

Chris Simpson
la source
Très bonne réponse. Pouvez-vous expliquer comment cela a fonctionné et comment s'appelle cet algorithme?
iMatoria
3
@iMatoria - Ceci est juste une version réduite du célèbre théorème de Pythagore. Étant donné deux ensembles de coordonnées, la différence entre les deux valeurs X représente un côté d'un triangle rectangle et la différence entre les valeurs Y est l'autre. Pour obtenir l'hypoténuse (et donc la distance entre les points), vous additionnez les carrés de ces deux valeurs ensemble puis la racine carrée du résultat. Dans notre cas, nous ne faisons pas le dernier bit (l'enracinement carré) parce que nous ne pouvons pas. Heureusement, ce n'est pas nécessaire pour un ordre de tri.
Chris Simpson
6
Dans mon application BostonBusMap, j'ai utilisé cette solution pour afficher les arrêts les plus proches de l'emplacement actuel. Cependant, vous devez mettre à l'échelle la distance de longitude cos(latitude)pour que la latitude et la longitude soient à peu près égales. Voir en.wikipedia.org/wiki/…
noisecapella
0

Afin d'augmenter les performances autant que possible, je suggère d'améliorer l'idée de @Chris Simpson avec la ORDER BYclause suivante :

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

Dans ce cas, vous devez transmettre les valeurs suivantes à partir du code:

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

Et vous devez également stocker LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2comme colonne supplémentaire dans la base de données. Remplissez-le en insérant vos entités dans la base de données. Cela améliore légèrement les performances lors de l'extraction d'une grande quantité de données.

Sergey Metlov
la source
-3

Essayez quelque chose comme ceci:

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

Après cela, id contient l'élément que vous voulez dans la base de données afin que vous puissiez le récupérer:

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

J'espère que ça t'as aidé!

Scott Helme
la source