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?
Réponses:
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
radius
mè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)./** * 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 de la base de données qui stocke les valeurs de latitude etCOL_Y
correspond à 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.
la source
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.
la source
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)
être inférieur àdistance
oudistance^2
?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.
la source
cos(latitude)
pour que la latitude et la longitude soient à peu près égales. Voir en.wikipedia.org/wiki/…Afin d'augmenter les performances autant que possible, je suggère d'améliorer l'idée de @Chris Simpson avec la
ORDER BY
clause 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^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 lors de l'extraction d'une grande quantité de données.la source
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é!
la source