La nouvelle base de données Firestore de Firebase prend-elle en charge de manière native les requêtes géographiques basées sur la localisation? trouver des messages dans un rayon de 10 miles ou trouver les 50 messages les plus proches?
Je vois qu'il existe des projets existants pour la base de données firebase en temps réel. Des projets tels que geofire pourraient-ils également être adaptés à Firestore?
UPDATE: Firestore ne prend pas en charge les requêtes GeoPoint actuelles. Par conséquent, si la requête ci-dessous s'exécute correctement, elle ne filtre que par latitude, et non par longitude; elle renverra donc de nombreux résultats qui ne sont pas proches. La meilleure solution serait d’utiliser geohashes . Pour apprendre à faire quelque chose de similaire vous-même, regardez cette vidéo .
Cela peut être fait en créant un cadre de sélection inférieur à supérieur à requête. En ce qui concerne l'efficacité, je ne peux pas en parler.
Notez que la précision de l'offset lat/long pour environ 1 mile devrait être examinée, mais voici un moyen rapide de procéder:
Swift 3.0 Version
func getDocumentNearBy(latitude: Double, longitude: Double, distance: Double) {
// ~1 mile of lat and lon in degrees
let lat = 0.0144927536231884
let lon = 0.0181818181818182
let lowerLat = latitude - (lat * distance)
let lowerLon = longitude - (lon * distance)
let greaterLat = latitude + (lat * distance)
let greaterLon = longitude + (lon * distance)
let lesserGeopoint = GeoPoint(latitude: lowerLat, longitude: lowerLon)
let greaterGeopoint = GeoPoint(latitude: greaterLat, longitude: greaterLon)
let docRef = Firestore.firestore().collection("locations")
let query = docRef.whereField("location", isGreaterThan: lesserGeopoint).whereField("location", isLessThan: greaterGeopoint)
query.getDocuments { snapshot, error in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in snapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
}
func run() {
// Get all locations within 10 miles of Google Headquarters
getDocumentNearBy(latitude: 37.422000, longitude: -122.084057, distance: 10)
}
UPDATE: Firestore ne prend pas en charge les requêtes GeoPoint actuelles. Par conséquent, si la requête ci-dessous est exécutée avec succès, elle ne filtre que par latitude, et non par longitude; elle renverra donc de nombreux résultats non proches. La meilleure solution serait d’utiliser geohashes . Pour apprendre à faire quelque chose de similaire vous-même, regardez cette vidéo .
(Tout d’abord, permettez-moi de vous présenter mes excuses pour tout le code contenu dans ce message. Je souhaitais simplement que tous les lecteurs de cette réponse aient la possibilité de reproduire facilement les fonctionnalités.)
Pour répondre à la même préoccupation que le PO avait, au début, j’ai adapté la bibliothèque GeoFire à l’utilisation de Firestore (vous pouvez en apprendre beaucoup sur la géolocalisation en consultant cette bibliothèque). Puis j'ai réalisé que cela ne me dérangeait pas vraiment si les emplacements étaient retournés dans un cercle exact. Je voulais juste un moyen d'obtenir des endroits «à proximité».
Je ne peux pas croire combien de temps cela m'a pris pour réaliser cela, mais vous pouvez simplement effectuer une requête de double inégalité sur un champ GeoPoint en utilisant un coin SW et un coin NE pour obtenir des emplacements dans un cadre de sélection autour d'un point central.
J'ai donc créé une fonction JavaScript comme celle ci-dessous (il s’agit d’une version JS de la réponse de Ryan Lee).
/**
* Get locations within a bounding box defined by a center point and distance from from the center point to the side of the box;
*
* @param {Object} area an object that represents the bounding box
* around a point in which locations should be retrieved
* @param {Object} area.center an object containing the latitude and
* longitude of the center point of the bounding box
* @param {number} area.center.latitude the latitude of the center point
* @param {number} area.center.longitude the longitude of the center point
* @param {number} area.radius (in kilometers) the radius of a circle
* that is inscribed in the bounding box;
* This could also be described as half of the bounding box's side length.
* @return {Promise} a Promise that fulfills with an array of all the
* retrieved locations
*/
function getLocations(area) {
// calculate the SW and NE corners of the bounding box to query for
const box = utils.boundingBoxCoordinates(area.center, area.radius);
// construct the GeoPoints
const lesserGeopoint = new GeoPoint(box.swCorner.latitude, box.swCorner.longitude);
const greaterGeopoint = new GeoPoint(box.neCorner.latitude, box.neCorner.longitude);
// construct the Firestore query
let query = firebase.firestore().collection('myCollection').where('location', '>', lesserGeopoint).where('location', '<', greaterGeopoint);
// return a Promise that fulfills with the locations
return query.get()
.then((snapshot) => {
const allLocs = []; // used to hold all the loc data
snapshot.forEach((loc) => {
// get the data
const data = loc.data();
// calculate a distance from the center
data.distanceFromCenter = utils.distance(area.center, data.location);
// add to the array
allLocs.Push(data);
});
return allLocs;
})
.catch((err) => {
return new Error('Error while retrieving events');
});
}
La fonction ci-dessus ajoute également une propriété .distanceFromCenter à chaque donnée d'emplacement renvoyée afin que vous puissiez obtenir le comportement semblable à un cercle en vérifiant simplement si cette distance est dans la plage souhaitée.
J'utilise deux fonctions util dans la fonction ci-dessus alors voici le code pour ceux-ci aussi. (Toutes les fonctions ci-dessous sont en réalité adaptées de la bibliothèque GeoFire.)
distance ():
/**
* Calculates the distance, in kilometers, between two locations, via the
* Haversine formula. Note that this is approximate due to the fact that
* the Earth's radius varies between 6356.752 km and 6378.137 km.
*
* @param {Object} location1 The first location given as .latitude and .longitude
* @param {Object} location2 The second location given as .latitude and .longitude
* @return {number} The distance, in kilometers, between the inputted locations.
*/
distance(location1, location2) {
const radius = 6371; // Earth's radius in kilometers
const latDelta = degreesToRadians(location2.latitude - location1.latitude);
const lonDelta = degreesToRadians(location2.longitude - location1.longitude);
const a = (Math.sin(latDelta / 2) * Math.sin(latDelta / 2)) +
(Math.cos(degreesToRadians(location1.latitude)) * Math.cos(degreesToRadians(location2.latitude)) *
Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2));
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}
boundingBoxCoordinates (): (Il y a d'autres utilitaires utilisés ici que j'ai collés ci-dessous.)
/**
* Calculates the SW and NE corners of a bounding box around a center point for a given radius;
*
* @param {Object} center The center given as .latitude and .longitude
* @param {number} radius The radius of the box (in kilometers)
* @return {Object} The SW and NE corners given as .swCorner and .neCorner
*/
boundingBoxCoordinates(center, radius) {
const KM_PER_DEGREE_LATITUDE = 110.574;
const latDegrees = radius / KM_PER_DEGREE_LATITUDE;
const latitudeNorth = Math.min(90, center.latitude + latDegrees);
const latitudeSouth = Math.max(-90, center.latitude - latDegrees);
// calculate longitude based on current latitude
const longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth);
const longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth);
const longDegs = Math.max(longDegsNorth, longDegsSouth);
return {
swCorner: { // bottom-left (SW corner)
latitude: latitudeSouth,
longitude: wrapLongitude(center.longitude - longDegs),
},
neCorner: { // top-right (NE corner)
latitude: latitudeNorth,
longitude: wrapLongitude(center.longitude + longDegs),
},
};
}
metresToLongitudeDegrees ():
/**
* Calculates the number of degrees a given distance is at a given latitude.
*
* @param {number} distance The distance to convert.
* @param {number} latitude The latitude at which to calculate.
* @return {number} The number of degrees the distance corresponds to.
*/
function metersToLongitudeDegrees(distance, latitude) {
const EARTH_EQ_RADIUS = 6378137.0;
// this is a super, fancy magic number that the GeoFire lib can explain (maybe)
const E2 = 0.00669447819799;
const EPSILON = 1e-12;
const radians = degreesToRadians(latitude);
const num = Math.cos(radians) * EARTH_EQ_RADIUS * Math.PI / 180;
const denom = 1 / Math.sqrt(1 - E2 * Math.sin(radians) * Math.sin(radians));
const deltaDeg = num * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360 : 0;
}
// else
return Math.min(360, distance / deltaDeg);
}
wrapLongitude ():
/**
* Wraps the longitude to [-180,180].
*
* @param {number} longitude The longitude to wrap.
* @return {number} longitude The resulting longitude.
*/
function wrapLongitude(longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
const adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
À ce jour, il n'y a aucun moyen de faire une telle requête. Il y a d'autres questions dans SO qui s'y rapportent:
Est-il possible d'utiliser GeoFire avec Firestore?
Est-il possible d'utiliser GeoFire avec Firestore?
Dans mon projet Android actuel, je peux utiliser https://github.com/drfonfon/Android-geohash pour ajouter un champ geohash pendant que l'équipe Firebase développe un support natif.
L'utilisation de la base de données en temps réel Firebase, comme suggéré dans d'autres questions, signifie que vous ne pouvez pas filtrer simultanément vos résultats par lieu et par d'autres champs. C'est la raison principale pour laquelle je souhaite passer à Firestore.
Un nouveau projet a été introduit depuis que @monkeybonkey a posé cette question pour la première fois. Le projet s'appelle GEOFirestore .
Avec cette bibliothèque, vous pouvez effectuer des requêtes telles que des documents de requête dans un cercle:
const geoQuery = geoFirestore.query({
center: new firebase.firestore.GeoPoint(10.38, 2.41),
radius: 10.5
});
Vous pouvez installer GeoFirestore via npm. Vous devrez installer Firebase séparément (car il s’agit d’une dépendance entre pairs de GeoFirestore):
$ npm install geofirestore firebase --save
Il existe actuellement une nouvelle bibliothèque pour iOS et Android qui permet aux développeurs d'exécuter des requêtes géographiques basées sur l'emplacement. La bibliothèque s'appelle GeoFirestore . J'ai déjà implémenté cette bibliothèque et j'ai trouvé beaucoup de documentation et aucune erreur. Cela semble bien testé et une bonne option à utiliser.
Détournant ce fil pour aider, espérons-le, tous ceux qui cherchent encore. Firestore ne prend toujours pas en charge les requêtes géolocalisées, et utiliser la bibliothèque GeoFirestore de Google n’est pas idéal non plus, car cela vous permettra uniquement de rechercher par emplacement, rien d’autre.
J'ai mis cela ensemble: https://github.com/mbramwell1/GeoFire-Android
Il vous permet essentiellement de faire des recherches à proximité en utilisant un lieu et une distance:
QueryLocation queryLocation = QueryLocation.fromDegrees(latitude, longitude);
Distance searchDistance = new Distance(1.0, DistanceUnit.KILOMETERS);
geoFire.query()
.whereNearTo(queryLocation, distance)
.build()
.get();
Il y a plus de docs sur le repo. Cela fonctionne pour moi, alors essayez-le, espérons qu'il fera ce dont vous avez besoin.
Pour Dart
///
/// Checks if these coordinates are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool coordinatesValid(double latitude, double longitude) {
return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180);
}
///
/// Checks if the coordinates of a GeopPoint are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool geoPointValid(GeoPoint point) {
return (point.latitude >= -90 &&
point.latitude <= 90 &&
point.longitude >= -180 &&
point.longitude <= 180);
}
///
/// Wraps the longitude to [-180,180].
///
/// [longitude] The longitude to wrap.
/// returns The resulting longitude.
///
double wrapLongitude(double longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
final adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
double degreesToRadians(double degrees) {
return (degrees * math.pi) / 180;
}
///
///Calculates the number of degrees a given distance is at a given latitude.
/// [distance] The distance to convert.
/// [latitude] The latitude at which to calculate.
/// returns the number of degrees the distance corresponds to.
double kilometersToLongitudeDegrees(double distance, double latitude) {
const EARTH_EQ_RADIUS = 6378137.0;
// this is a super, fancy magic number that the GeoFire lib can explain (maybe)
const E2 = 0.00669447819799;
const EPSILON = 1e-12;
final radians = degreesToRadians(latitude);
final numerator = math.cos(radians) * EARTH_EQ_RADIUS * math.pi / 180;
final denom = 1 / math.sqrt(1 - E2 * math.sin(radians) * math.sin(radians));
final deltaDeg = numerator * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360.0 : 0.0;
}
// else
return math.min(360.0, distance / deltaDeg);
}
///
/// Defines the boundingbox for the query based
/// on its south-west and north-east corners
class GeoBoundingBox {
final GeoPoint swCorner;
final GeoPoint neCorner;
GeoBoundingBox({this.swCorner, this.neCorner});
}
///
/// Defines the search area by a circle [center] / [radiusInKilometers]
/// Based on the limitations of FireStore we can only search in rectangles
/// which means that from this definition a final search square is calculated
/// that contains the circle
class Area {
final GeoPoint center;
final double radiusInKilometers;
Area(this.center, this.radiusInKilometers):
assert(geoPointValid(center)), assert(radiusInKilometers >= 0);
factory Area.inMeters(GeoPoint gp, int radiusInMeters) {
return new Area(gp, radiusInMeters / 1000.0);
}
factory Area.inMiles(GeoPoint gp, int radiusMiles) {
return new Area(gp, radiusMiles * 1.60934);
}
/// returns the distance in km of [point] to center
double distanceToCenter(GeoPoint point) {
return distanceInKilometers(center, point);
}
}
///
///Calculates the SW and NE corners of a bounding box around a center point for a given radius;
/// [area] with the center given as .latitude and .longitude
/// and the radius of the box (in kilometers)
GeoBoundingBox boundingBoxCoordinates(Area area) {
const KM_PER_DEGREE_LATITUDE = 110.574;
final latDegrees = area.radiusInKilometers / KM_PER_DEGREE_LATITUDE;
final latitudeNorth = math.min(90.0, area.center.latitude + latDegrees);
final latitudeSouth = math.max(-90.0, area.center.latitude - latDegrees);
// calculate longitude based on current latitude
final longDegsNorth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeNorth);
final longDegsSouth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeSouth);
final longDegs = math.max(longDegsNorth, longDegsSouth);
return new GeoBoundingBox(
swCorner: new GeoPoint(latitudeSouth, wrapLongitude(area.center.longitude - longDegs)),
neCorner: new GeoPoint(latitudeNorth, wrapLongitude(area.center.longitude + longDegs)));
}
///
/// Calculates the distance, in kilometers, between two locations, via the
/// Haversine formula. Note that this is approximate due to the fact that
/// the Earth's radius varies between 6356.752 km and 6378.137 km.
/// [location1] The first location given
/// [location2] The second location given
/// sreturn the distance, in kilometers, between the two locations.
///
double distanceInKilometers(GeoPoint location1, GeoPoint location2) {
const radius = 6371; // Earth's radius in kilometers
final latDelta = degreesToRadians(location2.latitude - location1.latitude);
final lonDelta = degreesToRadians(location2.longitude - location1.longitude);
final a = (math.sin(latDelta / 2) * math.sin(latDelta / 2)) +
(math.cos(degreesToRadians(location1.latitude)) *
math.cos(degreesToRadians(location2.latitude)) *
math.sin(lonDelta / 2) *
math.sin(lonDelta / 2));
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return radius * c;
}
Je viens de publier un paquet Flutter basé sur le code JS ci-dessus https://pub.dartlang.org/packages/firestore_helpers
Ce n'est pas encore complètement testé, cela devrait être un peu une amélioration par rapport à la réponse de Ryan Lee
Mon calcul est plus précis, puis je filtre les réponses pour éliminer les occurrences comprises dans le cadre de sélection, mais en dehors du rayon.
Swift 4
func getDocumentNearBy(latitude: Double, longitude: Double, meters: Double) {
let myGeopoint = GeoPoint(latitude:latitude, longitude:longitude )
let r_earth : Double = 6378137 // Radius of earth in Meters
// 1 degree lat in m
let kLat = (2 * Double.pi / 360) * r_earth
let kLon = (2 * Double.pi / 360) * r_earth * __cospi(latitude/180.0)
let deltaLat = meters / kLat
let deltaLon = meters / kLon
let swGeopoint = GeoPoint(latitude: latitude - deltaLat, longitude: longitude - deltaLon)
let neGeopoint = GeoPoint(latitude: latitude + deltaLat, longitude: longitude + deltaLon)
let docRef : CollectionReference = appDelegate.db.collection("restos")
let query = docRef.whereField("location", isGreaterThan: swGeopoint).whereField("location", isLessThan: neGeopoint)
query.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
self.documents = snapshot.documents.filter { (document) in
if let location = document.get("location") as? GeoPoint {
let myDistance = self.distanceBetween(geoPoint1:myGeopoint,geoPoint2:location)
print("myDistance:\(myDistance) distance:\(meters)")
return myDistance <= meters
}
return false
}
}
}
Fonctions qui mesurent avec précision la distance en mètres entre 2 géopoints pour le filtrage
func distanceBetween(geoPoint1:GeoPoint, geoPoint2:GeoPoint) -> Double{
return distanceBetween(lat1: geoPoint1.latitude,
lon1: geoPoint1.longitude,
lat2: geoPoint2.latitude,
lon2: geoPoint2.longitude)
}
func distanceBetween(lat1:Double, lon1:Double, lat2:Double, lon2:Double) -> Double{ // generally used geo measurement function
let R : Double = 6378.137; // Radius of earth in KM
let dLat = lat2 * Double.pi / 180 - lat1 * Double.pi / 180;
let dLon = lon2 * Double.pi / 180 - lon1 * Double.pi / 180;
let a = sin(dLat/2) * sin(dLat/2) +
cos(lat1 * Double.pi / 180) * cos(lat2 * Double.pi / 180) *
sin(dLon/2) * sin(dLon/2);
let c = 2 * atan2(sqrt(a), sqrt(1-a));
let d = R * c;
return d * 1000; // meters
}
Oui, c'est un sujet ancien, mais je veux aider uniquement sur le code Java. Comment j'ai résolu un problème de longitude? J'ai utilisé un code de Ryan Lee et Michael Teper .
Un code:
@Override
public void getUsersForTwentyMiles() {
FirebaseFirestore db = FirebaseFirestore.getInstance();
double latitude = 33.0076665;
double longitude = 35.1011336;
int distance = 20; //20 milles
GeoPoint lg = new GeoPoint(latitude, longitude);
// ~1 mile of lat and lon in degrees
double lat = 0.0144927536231884;
double lon = 0.0181818181818182;
final double lowerLat = latitude - (lat * distance);
final double lowerLon = longitude - (lon * distance);
double greaterLat = latitude + (lat * distance);
final double greaterLon = longitude + (lon * distance);
final GeoPoint lesserGeopoint = new GeoPoint(lowerLat, lowerLon);
final GeoPoint greaterGeopoint = new GeoPoint(greaterLat, greaterLon);
Log.d(LOG_TAG, "local general lovation " + lg);
Log.d(LOG_TAG, "local lesserGeopoint " + lesserGeopoint);
Log.d(LOG_TAG, "local greaterGeopoint " + greaterGeopoint);
//get users for twenty miles by only a latitude
db.collection("users")
.whereGreaterThan("location", lesserGeopoint)
.whereLessThan("location", greaterGeopoint)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
UserData user = document.toObject(UserData.class);
//here a longitude condition (myLocation - 20 <= myLocation <= myLocation +20)
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
}
} else {
Log.d(LOG_TAG, "Error getting documents: ", task.getException());
}
}
});
}
Juste à l'intérieur après avoir émis le résultat, définissez le filtre sur la longitude:
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
J'espère que cela aidera quelqu'un . Passez une bonne journée!
Il existe une bibliothèque GeoFire pour Firestore appelée Geofirestore: https://github.com/imperiumlabs/GeoFirestore (Avertissement: j'ai contribué à son développement). Il est super facile à utiliser et offre les mêmes fonctionnalités pour Firestore que Geofire pour Firebase Realtime DB)