web-dev-qa-db-fra.com

Comment exécuter une requête géo "à proximité" avec firestore?

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?

37
MonkeyBonkey

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)
}
33
Ryan Lee

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);
}
18
stparham

À 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?

Comment interroger les points géographiques les plus proches dans une collection de Firebase Cloud 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.

11
hecht

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
5
ra9r

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. 

4
Nikhil Sridhar

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.

3
Martin Bramwell

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

3
Thomas

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
}
1
Ryan Heitner

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!

0
Yury Matatov

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)

0
DHShah01