web-dev-qa-db-fra.com

SQlite Obtention des emplacements les plus proches (avec latitude et longitude)

J'ai des données avec latitude et 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, mon emplacement actuel - 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é d'é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), ce qui ajoute une taille inutile à l'application.

L'autre côté est que j'ai besoin de la fonction Order by de SQL, car l'affichage de la distance 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 n'ai pas la colonne de distance dans ma base de données. Cela signifierait la mise à jour de 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 serais également reconnaissant!

Je sais qu'il y a des tonnes d'applications Android là-bas qui utilisent cette fonction, mais quelqu'un peut-il expliquer la magie?.

Au fait, j'ai trouvé cette alternative: 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 une autre manière, pas si redondante?

81
Jure

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 filtre déterministe seuil et plus précis sur les données, il est préférable de calculer 4 emplacements qui sont à radius mètre du nord , ouest, est et sud de votre point central dans votre code Java puis vérifiez facilement par moins et plus que les opérateurs SQL (>, <) pour déterminer si vos points dans la base de données se trouvent ou non dans ce rectangle.

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

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;

    }

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_X Est le nom de la colonne dans la base de données qui stocke les valeurs de latitude et COL_Y Est pour la longitude.

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

2) Vous pouvez maintenant faire 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é.

106
Bobs

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), ce qui précède ne fonctionne qu'à l'équateur. Sous d'autres latitudes, vous devez réduire l'impact de la longitude sur l'ordre de tri. (Imaginez que vous êtes proche 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);

Commandez ensuite par:

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

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

67
Teasel

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 tout 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 consiste à renvoyer le lot et à trier et filtrer après coup, mais cela entraînerait un deuxième curseur et de nombreux résultats inutiles renvoyés et éliminé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 rooter les résultats au carré donc SQLite peut gérer le calcul.

MODIFIER:

Cette réponse reçoit toujours de l'amour. Cela fonctionne très 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 de "fudge" qui corrige les inexactitudes qui augmentent à mesure que la latitude approche 90.

66
Chris Simpson

Avez-vous envisagé une balise/index Geohash pour vos entrées afin de réduire la taille de votre jeu de résultats, puis appliquez la fonction appropriée.

Une autre question de stackoverflow dans une zone similaire: trouver le point le plus proche d'un point donné

2
Morrison Chang

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

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^2 comme 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 tout en extrayant une grande quantité de données.

0
Sergey Metlov