92 votes

Comment exécuter une requête géographique "à 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 l'emplacement ? Par exemple, trouver des messages dans un rayon de 15 km ou trouver les 50 messages les plus proches ?

Je vois qu'il y a des projets existants pour la base de données firebase en temps réel, des projets tels que geofire - pourraient-ils être adaptés à firestore également ?

65voto

Ryan Lee Points 513

MISE À JOUR : Firestore ne prend pas en charge les requêtes GeoPoint à l'heure actuelle. Ainsi, bien que la requête ci-dessous s'exécute avec succès, elle ne filtre que par la latitude, et non par la longitude, et renvoie donc de nombreux résultats qui ne sont pas à proximité. La meilleure solution serait d'utiliser geohashes . Pour apprendre à faire quelque chose de similaire vous-même, jetez un coup d'œil à ce qui suit vidéo .

Cela peut se faire en créant une boîte de délimitation inférieure à supérieure à la requête. Quant à l'efficacité, je ne peux pas en parler.

Remarque : la précision du décalage lat/long sur ~1 mile doit être vérifiée, mais voici un moyen rapide de le faire :

Version SWIFT 3.0

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

13 votes

Nous exécutons la même requête que vous, mais les résultats que nous obtenons ignorent la longitude. Ainsi, tous les documents que nous obtenons sont dans la plage de latitude, mais pas dans la plage de longitude.

0 votes

Quelqu'un a déjà trouvé un moyen de contourner ce problème ? @zirinisp

0 votes

@winston La seule solution consiste à utiliser la requête, puis à filtrer les résultats en fonction de la longitude.

32voto

stparham Points 320

MISE À JOUR : Firestore ne prend pas en charge les requêtes GeoPoint à l'heure actuelle. Ainsi, bien que la requête ci-dessous s'exécute avec succès, elle ne filtre que par la latitude, et non par la longitude, et renvoie donc de nombreux résultats qui ne sont pas à proximité. La meilleure solution serait d'utiliser geohashes . Pour apprendre à faire quelque chose de similaire vous-même, jetez un coup d'œil à ce qui suit vidéo .

(Tout d'abord, permettez-moi de m'excuser pour tout le code dans ce post, je voulais juste que quiconque lisant cette réponse ait un moment facile pour reproduire la fonctionnalité).

Pour répondre à la même préoccupation que celle du PO, j'ai d'abord adapté la Bibliothèque GeoFire pour fonctionner avec Firestore (vous pouvez apprendre beaucoup de choses sur la géographie en consultant cette bibliothèque). Puis j'ai réalisé que ça ne me dérangeait pas vraiment que les lieux soient retournés dans un cercle exact. Je voulais simplement trouver un moyen d'obtenir des lieux "proches".

Je n'en reviens pas du temps qu'il m'a fallu pour m'en rendre compte, mais il suffit d'effectuer une requête à double inégalité sur un champ GeoPoint en utilisant un coin SO et un coin NE pour obtenir des emplacements dans une boîte de délimitation autour d'un point central.

J'ai donc créé une fonction JavaScript comme celle qui suit (il s'agit en fait 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 élément de données de localisation renvoyé, afin que vous puissiez obtenir un comportement de type circulaire en vérifiant simplement si cette distance se situe dans la plage souhaitée.

J'utilise deux fonctions util dans la fonction ci-dessus, voici donc le code de ces fonctions. (Toutes les fonctions util ci-dessous sont en fait 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 plus d'utilitaires utilisés ici aussi que j'ai collé 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),
    },
  };
}

metersToLongitudeDegrees() :

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

0 votes

Bonjour, il vous manque la méthode degreesToRadians

4 votes

function degreesToRadians(degrees) {return (degrees * Math.PI)/180;}

1 votes

Mais alors vous ne pouvez pas appliquer les filtres de plage à d'autres champs dans la même requête :/

17voto

raiglstorfer Points 1118

Un nouveau projet a été introduit depuis que @monkeybonkey a posé cette question. Ce projet s'appelle GEOFirestore .

Grâce à cette bibliothèque, vous pouvez effectuer des requêtes telles que l'interrogation de documents à l'intérieur d'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 homologue à GeoFirestore) :

$ npm install geofirestore firebase --save

1 votes

Ceci est pratiquement inutile puisqu'il ne fonctionne qu'en Javascript.

7 votes

@nikhilSridhar c'est particulièrement utile ! Vous voulez garder ce type de requêtes cachées à l'utilisateur final et les exécuter uniquement dans Cloud Functions ! 1. La requête elle-même peut déclencher plusieurs demandes, alors que vous voulez que l'appareil mobile en fasse le moins possible. 2. L'exposition de ce type de données directement à l'application mobile peut devenir une menace sérieuse pour la sécurité ! Disons que vous stockez les emplacements de tous les utilisateurs... si vous donnez à l'application le pouvoir d'exécuter ce type de requête, vous donnez également ce pouvoir à tout développeur qui veut espionner l'emplacement de tous vos utilisateurs.

2 votes

@NikhilSridhar JavaScript est le langage le plus utilisé sur la planète, comment peut-il être inutile ? (Avec TypeScript bien sûr). Si vous écrivez dans un autre langage (côté serveur ou client), vous perdez votre temps à mon avis. Code client/serveur partagé, quelqu'un ? React-native, etc etc.

11voto

hecht Points 252

À ce jour, il n'existe aucun moyen d'effectuer une telle requête. Il y a d'autres questions dans SO qui y sont liées :

Y a-t-il un moyen d'utiliser GeoFire avec Firestore ?

Comment interroger les GeoPoints les plus proches dans une collection dans Firebase Cloud Firestore ?

Y a-t-il un moyen 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 de Firebase développe un support natif.

L'utilisation de la base de données Firebase Realtime comme suggéré dans d'autres questions signifie que vous ne pouvez pas filtrer votre ensemble de résultats par emplacement et d'autres champs simultanément, la principale raison pour laquelle je veux passer à Firestore en premier lieu.

5voto

Nikhil Sridhar Points 677

Il existe actuellement une nouvelle bibliothèque pour iOS et Android qui permet aux développeurs d'effectuer des requêtes géographiques basées sur la localisation. Cette bibliothèque s'appelle GeoFirestore . J'ai déjà mis en œuvre cette bibliothèque et j'ai trouvé beaucoup de documentation et aucune erreur. Elle semble bien testée et constitue une bonne option à utiliser.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X