97

Does the new firestore database from firebase natively support location based geo queries? i.e. Find posts within 10 miles, or find the 50 nearest posts?

I see that there are some existing projects for the real-time firebase database, projects such as geofire- could those be adapted to firestore as well?

Dan McGrath
  • 41,220
  • 11
  • 99
  • 130
MonkeyBonkey
  • 46,433
  • 78
  • 254
  • 460
  • 5
    Possible duplicate of [How to query closest GeoPoints in a collection in Firebase Cloud Firestore?](https://stackoverflow.com/questions/46607760/how-to-query-closest-geopoints-in-a-collection-in-firebase-cloud-firestore) – Frank van Puffelen Oct 08 '17 at 15:37

14 Answers14

67

UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use geohashes. To learn how to do something similar yourself, have a look at this video.

This can be done by creating a bounding box less than greater than query. As for the efficiency, I can't speak to it.

Note, the accuracy of the lat/long offset for ~1 mile should be reviewed, but here is a quick way to do this:

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)
}
Michael Teper
  • 4,591
  • 2
  • 32
  • 49
Ryan Lee
  • 932
  • 7
  • 8
  • 13
    we are running the same query as you do, but the results we are getting are ignoring longitude. So all the documents we get are within the lat range, but not within the lon range. – zirinisp Mar 11 '18 at 13:28
  • anyone find a way around this issue yet? @zirinisp – winston Apr 21 '18 at 17:38
  • @winston The only workaround is to use the query and then filter the results for longitude. – zirinisp Apr 24 '18 at 07:47
  • @zirinisp thanks for the reply. Can you please elaborate? Do you mean filter the results with another where clause to include a specific value for longitude? – winston Apr 25 '18 at 22:57
  • 1
    @winston have a look at the following link. I hope it helps, as I have a custom implementation of Firecloud with Promises. I tried to simplify it. https://gist.github.com/zirinisp/e5cf5d9c33cb0bd815993900618eafe0 – zirinisp Apr 27 '18 at 08:32
  • Thank you so much for sharing this. I'm studying this now and it's starting to make sense. – winston Apr 28 '18 at 00:58
  • I see in the example that the range queries across a single field "GeoPoint" is that a built in data type? If I'm using javascript should I new up a GeoPoint object or is it stored as an array? – MonkeyBonkey May 03 '18 at 16:25
  • ok, I see that GeoPoint is a class and I should store coordinates in this geo type – MonkeyBonkey May 10 '18 at 15:35
  • 2
    @zirinisp For the longitude issue I set up an extra field in the collection whith is a virtual longitude tile. I just do Math.floor(longitude) and then on querying you can do .where("longitude_tile", "==", Math.floor(yourCurrentLongitude)). Since this is an equal operator, you can use it with the location range to greatly filter the results on the server instead of the client. Every degree of longitude is around 69 miles which serves for ourpurpose well enough. – sebastianf182 May 19 '18 at 23:43
  • 1
    @sfratini but what if some items will be on the border of this rounded longitude. They can be several meters away but not displaying by this query. – rendom May 21 '18 at 17:08
  • That is true. It is a con about this. It is acceptable in my scenario. – sebastianf182 May 21 '18 at 17:13
  • 6
    Why this answer marked as Correct if it is in fact *not* solving the question? – rendom May 22 '18 at 14:12
  • 4
    Thanks for this answer! As other folks have mentioned, there is a large caveat though. Would you be willing to add something like this to the top? UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use [geohashes](https://en.wikipedia.org/wiki/Geohash). To learn how to do something similar yourself, have a look at this [video](https://www.youtube.com/watch?v=mx1mMdHBi5Q). – Michael Lehenbauer Nov 07 '18 at 22:29
  • I think this is quite an acceptable solution if you don't have an insane amount of results. Looking at the geohashes i'm not sure is more efficient because it has lots of queries, i even broke the geofire lib trying to make it work with firestore and still not completely sure how it works. I'm saving myself time and hope they'll introduce proper geopoints queries in the near future. – Cristi Băluță Jan 03 '19 at 10:54
  • // ~1 km of lat and lon in degrees double lat = 0.009; double lon = 0.0001; – Sumer Singh Oct 06 '19 at 03:26
  • It's still not support actual GeoPoint queries? I'm trying to get values stored in my firestore database within 75 miles of current location of user using geopoint but it' not working. @MichaelLehenbauer – Divyesh Jun 23 '20 at 06:18
32

UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use geohashes. To learn how to do something similar yourself, have a look at this video.

(First let me apologize for all the code in this post, I just wanted anyone reading this answer to have an easy time reproducing the functionality.)

To address the same concern the OP had, at first I adapted the GeoFire library to work with Firestore (you can learn a lot about geo-stuff by looking at that library). Then I realized I didn't really mind if locations were returned in an exact circle. I just wanted some way to get 'nearby' locations.

I can't believe how long it took me to realize this, but you can just perform a double inequality query on a GeoPoint field using a SW corner and NE corner to get locations within a bounding box around a center point.

So I made a JavaScript function like the one below (this is basically a JS version of Ryan Lee's answer).

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

The function above also adds a .distanceFromCenter property to each piece of location data that's returned so that you could get the circle-like behavior by just checking if that distance is within the range you want.

I use two util functions in the function above so here's the code for those as well. (All of the util functions below are actually adapted from the GeoFire library.)

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(): (There are more utils used in here as well that I've pasted below.)

/**
 * 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);
}
Ishara Madhawa
  • 3,549
  • 5
  • 24
  • 42
stparham
  • 475
  • 5
  • 7
  • Hi, you are missing degreesToRadians method – Karlo A. López Mar 19 '18 at 23:47
  • 4
    `function degreesToRadians(degrees) {return (degrees * Math.PI)/180;}` – Karlo A. López Mar 20 '18 at 01:28
  • 1
    But then you cannot apply range filters to other fields in the same query :/ – b-fg Apr 06 '18 at 10:17
  • 2
    @b-fg That's true. But at least for my application, after retrieving nearby locations from the database, my result set is usually small enough so that client-side filtering/sorting is practical. I know this isn't a perfect solution, but until geo-queries are directly supported by Firestore, I doubt there will be a perfect solution. – stparham Apr 18 '18 at 10:04
  • 1
    Thanks for this answer! Filtering by SW / NE corner unfortunately comes with a large caveat though. Would you be willing to add something like this to the top? UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use [geohashes](https://en.wikipedia.org/wiki/Geohash). To learn how to do something similar yourself, have a look at this [video](https://www.youtube.com/watch?v=mx1mMdHBi5Q). – Michael Lehenbauer Nov 07 '18 at 22:32
  • Thank you so much! – Mrigank Pawagi Jun 05 '20 at 17:27
17

A new project has been introduced since @monkeybonkey first ask this question. The project is called GEOFirestore.

With this library you can perform queries like query documents within a circle:

  const geoQuery = geoFirestore.query({
    center: new firebase.firestore.GeoPoint(10.38, 2.41),
    radius: 10.5
  });

You can install GeoFirestore via npm. You will have to install Firebase separately (because it is a peer dependency to GeoFirestore):

$ npm install geofirestore firebase --save
ra9r
  • 4,528
  • 4
  • 42
  • 52
  • 1
    This is practically useless since it only works in Javascript – Nikhil Sridhar Jul 10 '18 at 06:21
  • 7
    @nikhilSridhar it's particularly useful! You want to keep this kind of queries hidden away from the end user and execute them only in Cloud Functions! 1. The query itself might trigger multiple requests, while you want the mobile device to make as few as possible 2. Exposing this kind of data directly to mobile app can become a serious security threat! Say you store the locations of all users... if you give the app the power to run this kind of query, you also give that power to any developer who wants to spy on the location all of your users. – Kamil Janowski Nov 11 '18 at 13:19
  • 3
    @NikhilSridhar JavaScript is the most used language on the planet, how can it be useless? (With TypeScript of course). If you're writing any other language (server side or client) you're wasting your time in my opinion. Shared client/server code anyone? React-native, etc etc. – Oliver Dixon Jan 24 '19 at 16:26
11

As of today, there is no way to do such a query. There are other questions in SO related to it:

Is there a way to use GeoFire with Firestore?

How to query closest GeoPoints in a collection in Firebase Cloud Firestore?

Is there a way to use GeoFire with Firestore?

In my current Android project I may use https://github.com/drfonfon/android-geohash to add a geohash field while Firebase team is developing native support.

Using Firebase Realtime Database like suggested in other questions means that you can't filter your results set by location and other fields simultaneously, the main reason I want to switch to Firestore in the first place.

hecht
  • 292
  • 2
  • 6
8

As of late 2020 there is now also documentation of how to do geoqueries with Firestore.

These solutions for iOS, Android, and Web, build on top of a slimmed down version of the Firebase-created GeoFire libraries, and then show how to:

  • Generate geohash values and store them in Firestore
  • Determine geohash ranges of the bounding box for a certain point and radius
  • Perform queries across these geohash ranges

This a bit more low-level than most of the other libraries presented here, so it may be a better fit for some use-cases and a worse fit for others.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
7

Hijacking this thread to hopefully help anyone still looking. Firestore still does not support geo-based queries, and using the GeoFirestore library isnt ideal either as it will only let you search by location, nothing else.

I've put this together: https://github.com/mbramwell1/GeoFire-Android

It basically lets you do nearby searches using a location and distance:

QueryLocation queryLocation = QueryLocation.fromDegrees(latitude, longitude);
Distance searchDistance = new Distance(1.0, DistanceUnit.KILOMETERS);
geoFire.query()
    .whereNearTo(queryLocation, distance)
    .build()
    .get();

There are more docs on the repo. Its working for me so give it a try, hopefully it will do what you need.

pushkin_
  • 5
  • 1
  • 2
4

For 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;
}

I just published a Flutter package based on the JS code above https://pub.dartlang.org/packages/firestore_helpers

Thomas
  • 8,397
  • 7
  • 29
  • 39
  • 3
    Please don't just post some tool or library as an answer. At least demonstrate [how it solves the problem](http://meta.stackoverflow.com/a/251605) in the answer itself. – Zoe Jun 30 '18 at 15:24
  • the lib has a good documentation and it just an additional answer – Thomas Jun 30 '18 at 15:32
  • 1
    Sorry but this one of the typical SO moderations. It contains valuable information and the link will stay valid as this is a package repository which don't unlist any packages. Do delete it will deprive other users – Thomas Jul 02 '18 at 13:07
  • 1
    @Thomas a link always has the potential to die and not only that there are those who sit behind strict firewalls that may stop them from accessing the link. Just FYI on that, it's always good to bring relevant content into the answer. Not only that, it just looks substantially better. – Bugs Jul 02 '18 at 13:41
  • Note, that as of now, this library does not use geohashes and uses client-side filtering for longitudes, which isn't optimized – Ihor Klimov Feb 03 '19 at 20:35
  • Hello Thomas why is your package only for Flutter and not for Native Dart? – Alexander Sidikov Pfeif Oct 05 '20 at 09:38
  • The problem is that the Firestore package is Flutter specific. But you could just copy the source – Thomas Oct 05 '20 at 12:06
3

Yes, this is an old topic, but I want to help only on Java code. How I solved a problem with longitude? I used a code from Ryan Lee and Michael Teper.

A 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());
                    }
                }
            });
}

Just inside after issuing the result set the filter to longitude:

if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
    Log.d(LOG_TAG, "location: " + document.getId());
}  

I hope this will help someone. Have a nice day!

Yury Matatov
  • 805
  • 3
  • 11
  • 23
2

You should use GeoFire (works with Firestore). With this you can filter documents on server and read less documents from your Firestore db. This will reduce your read count as well.

Check this lib for GroFire: https://github.com/patpatchpatrick/GeoFirestore-iOS

"patpatchpatrick" made this to Swift 5 compatible.

Just do a pod install as follows:

pod 'Geofirestore', :git => 'https://github.com/patpatchpatrick/GeoFirestore-iOS'

I am using this library in one of my projects and it works fine.

To set a location:

let location: CLLocation = CLLocation(latitude: lat, longitude: lng)
yourCollection.setLocation(location: location, forDocumentWithID: "YourDocId") { (error) in }

To remove location:

collection.removeLocation(forDocumentWithID: "YourDocId")

To get docs:

let center = CLLocation(latitude: lat, longitude: lng)
let collection = "Your collection path"
let circleQuery = collection.query(withCenter: center, radius: Double(yourRadiusVal))
        
let _ = circleQuery.observe(.documentEntered, with: { (key, location) in
        //Use info as per your need
})

I have used .documentEntered, you can use other available geo queries like (Document Exited, Document Moved) as per your need.

You can query using GeoPoint as well.

Mohit Kumar
  • 2,898
  • 3
  • 21
  • 34
1

This is not fully tested yet it should be a bit of an improvement on Ryan Lee's answer

My calculation is more accurate and then I filter the answers to remove hits which fall within the bounding box but outside the radius

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

Functions which accurately measure the distance in Meters between 2 Geopoints for filtering

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
}
Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
1

The easiest way is to calculate a "geo hash" when storing the location in the database.

A geo hash is a string which represents a location down to a certain accuracy. The longer the geo hash, the closer the locations with said geo hash must be. Two location which are e.g. 100m apart may have the same 6-char geo hash but when calculating a 7-char geo hash the last char might be different.

There are plenty libraries allowing you to calculate geo hashes for any language. Just store it alongside the location and use a == query to find locations with the same geo hash.

crysxd
  • 3,177
  • 20
  • 32
1

In javascript you can simply

const db = firebase.firestore();

 //Geofire
import { GeoCollectionReference, GeoFirestore, GeoQuery, GeoQuerySnapshot } from 'geofirestore';

// Create a GeoFirestore reference
const geofirestore: GeoFirestore = new GeoFirestore(db);

// Create a GeoCollection reference
const geocollection: GeoCollectionReference = geofirestore.collection('<Your_collection_name>');

const query: GeoQuery = geocollectionDrivers.near({ 
        center: new firebase.firestore.GeoPoint(location.latitude, location.longitude), 
        radius: 10000 
    });
    query.onSnapshot(gquerySnapshot => {
        gquerySnapshot.forEach(res => {
            console.log(res.data());
        })
    });
Saad Zafar
  • 238
  • 3
  • 8
1

A workaround for Flutter till we have native query in Firestore to pull ordered documents based on lat/long: https://pub.dev/packages/geoflutterfire A plugin to store geo hashes in the Firestore and query the same.

Limitations: limit not supported

Krishna Shetty
  • 1,361
  • 4
  • 18
  • 39
0

There's a GeoFire library for Firestore called Geofirestore: https://github.com/imperiumlabs/GeoFirestore (Disclaimer: I helped develop it). It's super easy to use and offers the same features for Firestore that Geofire does for Firebase Realtime DB)

DHShah01
  • 553
  • 1
  • 7
  • 16
  • can you help fix it again because it brings down the firebasefirestore version and i'd geta an error that something is missing. – Jules Lee Aug 04 '19 at 12:16