How to get distance between users in listview flutter? - firebase

I have a firestore collection with one of the fields of documents containing locations(geopoints) of users, I want build a listview with distance between current user and all other users in ascending order. How do I achieve in flutter?
It looks something like this https://i.stack.imgur.com/yXfME.png
Update
How can I get location from document in a list?

STEP 1 : Get the distance of current user with present list item in kilometers
var distanceInKms = GetDistance(latitude1, longitude1, latitude2, longitude2);
double GetDistance(double lat1, double lon1, double lat2, double lon2)
{
var p = 0.017453292519943295;
var c = cos;
var a = 0.5 - c((lat2 - lat1) * p)/2 +
c(lat1 * p) * c(lat2 * p) *
(1 - c((lon2 - lon1) * p))/2;
return 12742 * asin(sqrt(a));
}
STEP 2 : Add these values to the list item object. And Sort the list based on this field.
myList.sort((a,b) => a.distance.compareTo(b.distance));
Refer this question for further info on sorting.
Question: Hi, thanks how do i return this in listview and how do I get document from firestore in a list?
Answer :
QuerySnapshot snapshot = await Firestore.instance.collection("YOUR_COLLECTION_NAME").getDocuments();
List<items> myList = [];
snapshot.documents.forEach((doc) {
//Convert incoming JSON info to object of your type and add to list
myList.add(List.fromJSON(doc.data));
});
//Apply logic on myList for sorting based on location

Related

Flutter Firestore pagination in abstract service class

I'm implementing pagination for my Flutter app with Firestore and I am running into a design issue.
I'm using services class to abstract database operation from the business logic of my app through data model class like so:
UI <- business logic (riverpod) <- data model class <- stateless firestore service
This works great as it follows the separation of concerns principles.
However, in the Firestore library, the only way to implement pagination is to save the last DocumentSnapshot to reference it in the next query using startAfterDocument(). This means, as my database services are stateless, I would need to save this DocumentSnapshot in my business logic code, who should in principle be completely abstracted from Firestore.
My first instinct would be to reconstruct a DocumentSnapshot from my data model class inside the service and use that for the pagination, but I would not be able to reconstruct it completely so I wonder if that would be enough.
Has anyone run into this issue? How did you solve it?
Cheers!
I stumbled upon the exact same issue, even though I was using Bloc instead of Riverpod.
I wrote a whole article on that, in order to support also live updates to the list and allowing infinite scrolling: ARTICLE ON MEDIUM
My approach was to order the query by name and id (for example), and using startAfter instead of startAfterDocument.
For example:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:infite_firestore_list/domain/list_item_entity.dart';
import 'package:infite_firestore_list/domain/item_repository.dart';
class FirebaseItemRepository implements ItemRepository {
final _itemsCollection = FirebaseFirestore.instance.collection('items');
#override
Future<Stream<List<ListItem>>> getItems({
String startAfterName = '',
String startAfterId = '',
int paginationSize = 10,
}) async {
return _itemsCollection
.orderBy("name")
.orderBy(FieldPath.documentId)
.startAfter([startAfterName, startAfterId])
.limit(paginationSize)
.snapshots()
.map((querySnapshot) => querySnapshot.docs.map((doc) {
return ListItemDataModel.fromFirestoreDocument(doc).toDomain();
}).toList());
}
}
in this way in your logic you only have to use id and name or whatever fields you wish to use, for example a date.
If you use a combination of multiple orderBy, the first time you run the query, Firebase may ask you to build the index with a link that will appear in the logs.
The drawback of this approach is that it only works if you are sure that the fields you are using in the orderBy are uniques. In fact, if for example you sort by date, if two fields have the same date and you use startAfter that date (first item), you may skip the second item with the same date...
In my example, the startAfterId doesn't seem useful, but in the usecase I had, it solved some edgecases I stumbled upon.
Alternative
An alternative I thought but that I personally didn't like (hence I did not mention it in my article) could be to store an array of the snapshots of the last documents of each page in the repository itself.
Than use the id from the logic domain to request a new page and make the correspondance id <--> snapshot in the repository itself.
This approach could be interesting if you are expecting a finite amount of pages and hence a controlled array in your repository singleton, otherwise it smell memory leaking and that's why I personally do not like this approach to stay as general as possible.
The very definition of paging (you are at one page; you go to the next page) is Stateful, so attempting to do it "stateless" has no meaning.
I don't work in flutter, but in JS/React I built the following class that returns an OBJECT that has the PageForward/PageBack methods, and properties to hold the required data/state:
export class PaginateFetch {
/**
* constructs an object to paginate through large Firestore Tables
* #param {string} table a properly formatted string representing the requested collection
* - always an ODD number of elements
* #param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
* The array is assumed to be sorted in the correct order -
* i.e. filterArray[0] is added first; filterArray[length-1] last
* returns data as an array of objects (not dissimilar to Redux State objects)
* with both the documentID and documentReference added as fields.
* #param {array} sortArray a 2xn array of sort (i.e. "orderBy") conditions
* #param {?string} refPath (optional) allows "table" parameter to reference a sub-collection
* of an existing document reference (I use a LOT of structured collections)
* #param {number} limit page size
* #category Paginator
*/
constructor(
table,
filterArray = null,
sortArray = null,
refPath = null,
limit = PAGINATE_DEFAULT
) {
const db = dbReference(refPath);
/**
* current limit of query results
* #type {number}
*/
this.limit = limit;
/**
* underlying query for fetch
* #private
* #type {Query}
*/
this.Query = sortQuery(
filterQuery(db.collection(table), filterArray),
sortArray
);
/**
* current status of pagination
* #type {PagingStatus}
* -1 pending; 0 uninitialized; 1 updated;
*/
this.status = PAGINATE_INIT;
}
/**
* executes the query again to fetch the next set of records
* #async
* #method
* #returns {Promise<RecordArray>} returns an array of record - the next page
*/
PageForward() {
const runQuery = this.snapshot
? this.Query.startAfter(last(this.snapshot.docs))
: this.Query;
this.status = PAGINATE_PENDING;
return runQuery
.limit(this.limit)
.get()
.then((QuerySnapshot) => {
this.status = PAGINATE_UPDATED;
//*IF* documents (i.e. haven't gone beyond start)
if (!QuerySnapshot.empty) {
//then update document set, and execute callback
//return Promise.resolve(QuerySnapshot);
this.snapshot = QuerySnapshot;
}
return Promise.resolve(RecordsFromSnapshot(this.snapshot));
});
}
/**
* executes the query again to fetch the previous set of records
* #async
* #method
* #returns {Promise<RecordArray>} returns an array of record - the next page
*/
PageBack() {
const runQuery = this.snapshot
? this.Query.endBefore(this.snapshot.docs[0])
: this.Query;
this.status = PAGINATE_PENDING;
return runQuery
.limitToLast(this.limit)
.get()
.then((QuerySnapshot) => {
this.status = PAGINATE_UPDATED;
//*IF* documents (i.e. haven't gone back ebfore start)
if (!QuerySnapshot.empty) {
//then update document set, and execute callback
this.snapshot = QuerySnapshot;
}
return Promise.resolve(RecordsFromSnapshot(this.snapshot));
});
}
}
/**
* #private
* #typedef {Object} filterObject
* #property {!String} fieldRef
* #property {!String} opStr
* #property {any} value
*/
/**
* ----------------------------------------------------------------------
* #private
* #function filterQuery
* builds and returns a query built from an array of filter (i.e. "where")
* conditions
* #param {Query} query collectionReference or Query to build filter upong
* #param {?filterObject} [filterArray] an (optional) 3xn array of filter(i.e. "where") conditions
* #returns {Query} Firestore Query object
*/
const filterQuery = (query, filterArray = null) => {
return filterArray
? filterArray.reduce((accQuery, filter) => {
return accQuery.where(filter.fieldRef, filter.opStr, filter.value);
}, query)
: query;
};
/**
* #private
* #typedef {Object} sortObject
* #property {!String} fieldRef
* #property {!String} dirStr
*/
/**
* ----------------------------------------------------------------------
* #private
* #function sortQuery
* builds and returns a query built from an array of filter (i.e. "where")
* conditions
* #param {Query} query collectionReference or Query to build filter upong
* #param {?sortObject} [sortArray] an (optional) 2xn array of sort (i.e. "orderBy") conditions
* #returns Firestore Query object
*/
const sortQuery = (query, sortArray = null) => {
return sortArray
? sortArray.reduce((accQuery, sortEntry) => {
return accQuery.orderBy(sortEntry.fieldRef, sortEntry.dirStr || "asc");
//note "||" - if dirStr is not present(i.e. falsy) default to "asc"
}, query)
: query;
};
If you are using or can use any orderBy queries. You can use startAfter with your last queries value. For example if you orderBy date you can use last date for your next pagination query.
startAfter method reference

How to sort list of locations in flutter from Firestore?

I've got a collection in my firebase firestore database called 'bakeries' and inside the collection has a series of documents, each with a geopoint field, where I typed in their latitudes and longitudes. To access them and see the nearest bakeries to the users, I created a ListView.builder. But, I'm trying to sort the bakeryList by its geopoint in relation to the user's current location and it's not working. I've tried this:
bakeryList.sort((a, b){
return a['geopoint'].compareTo(myLocation);
});
But it's not returning the nearest bakeries. Any ideas would be hugely appreciated!
final userLocation = Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
StreamSubscription<QuerySnapshot> subscription;
List <DocumentSnapshot> bakeryList;
final Query collectionReference = Firestore.instance.collection(('bakeries'));
#override
void initState() {
super.initState();
final myLocation = Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
subscription = collectionReference.snapshots().listen((data) {
setState(() {
bakeryList = data.documents;
});
});
}
#override
Widget build(BuildContext context) {
return bakeryList != null ?
ListView.builder(
itemCount: bakeryList.length,
itemBuilder: (context, index){
String imgPath = bakeryList[index].data['image'];
String bakeryTextPath = bakeryList[index].data['name'];
String locationNamePath = bakeryList[index].data['location name'];
GeoPoint geoPointPath = bakeryList[index].data['geopoint'];
final geolocation = Text('${geoPointPath.latitude}, ${geoPointPath.longitude}');
return BakeryCard(
etaText: '${geoPointPath.latitude}, ${geoPointPath.longitude}',
locationText: locationNamePath,
merchantText: bakeryTextPath,
assetImage: Image.network(imgPath),
function: (){});
})
: Center(child: CircularProgressIndicator(),
);
}
I think your problem is in your sort function:
bakeryList.sort((a, b){
return a['geocode'].compareTo(myLocation);
});
Here you are just comparing the position of the first bakery with the user's location. I think what you really want is something like:
bakeryList.sort((a, b){
return distance(a['geocode'], myLocation).compareTo(distance(b['geocode'], myLocation));
});
where the function distance gives you the distance between two points.
In addition to Diviloper's answer, please use the following function to sort items as per distance :
double calculateDistance(lat1, lon1, lat2, lon2) {
var p = 0.017453292519943295;
var c = cos;
var a = 0.5 -
c((lat2 - lat1) * p) / 2 +
c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2;
return 12742 * asin(sqrt(a));
}
This will ensure accurate results.
Note : Import following
import 'dart:math' show cos, sqrt, asin;
Sorry, it’s not a straight answer to your question, but you may want to try a GeoFlutterFire. It’s super optimized to work with this type of data, and getting a list of the nearest locations is very simple. Link to the package GeoFlutterFire.

How to query Firestore data 'sort by distance' without querying whole data [duplicate]

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?
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)
}
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);
}
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
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.
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.
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.
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
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!
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.
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
}
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.
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());
})
});
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
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)

Markers along a route in HERE Maps, RouteBoxer port

I am looking to get this:
When I used Google Maps API there was a "plugin" called RouteBoxer that created a grid along the route and after I could build a query using the bounds of each rectangle.
I have seen that there is a port of that Google Library for LeafLet but I haven't found a port of RouteBoxer library for HERE Maps.
Do exist another way to do that in HERE Maps?
Extended explanation of routeboxer way: how it works
Thank you,
Regards,
WIP EDIT: I'm porting the Google Library by myself. I almost get it but left calculate box intersections correctly...I am here just right now:
Finally I have ported by myself the original Google Routeboxer library to Here Maps v3 3.0 successfully.
Result:
Tips for classes:
google.maps.LatLngBounds is similar to H.geo.Rect
google.maps.LatLng is similar to H.geo.Point
Tips for methods:
google.maps.LatLngBounds.extend is similar to H.geo.Rect.mergeLatLng
New methods prototyped for H.geo.Rect:
getNorthEast() and getSouthWest()
Usage:
Include here-routeboxer.js after Here Maps JS api call and:
// after recieve route response
var route = result.response.route[0];
var path = route.shape;
var path_= [];
// Transform original path to an array of H.geo.Point
// TODO: create a simplified path for better perfomance
path.forEach(function(point) {
var parts = point.split(',');
path_.push(new H.geo.Point(parts[0], parts[1]));
});
var routeBoxer = new RouteBoxer();
var boxes = routeBoxer.box(path_, 3); // params: path and distance
// now use the boxes as you want :)
here-routeboxer.js:
/**
* #name Here-RouteBoxer
* #version 1.0
*
* based on
*
* #name RouteBoxer
* #version 1.0
* #copyright (c) 2010 Google Inc.
* #author Thor Mitchell
*
* #fileoverview The RouteBoxer class takes a path, such as the Polyline for a
* route generated by a Directions request, and generates a set of LatLngBounds
* objects that are guaranteed to contain every point within a given distance
* of that route. These LatLngBounds objects can then be used to generate
* requests to spatial search services that support bounds filtering (such as
* the Google Maps Data API) in order to implement search along a route.
* <br/><br/>
* RouteBoxer overlays a grid of the specified size on the route, identifies
* every grid cell that the route passes through, and generates a set of bounds
* that cover all of these cells, and their nearest neighbours. Consequently
* the bounds returned will extend up to ~3x the specified distance from the
* route in places.
*/
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Creates a new RouteBoxer
*
* #constructor
*/
function RouteBoxer() {
this.R = 6371; // earth's mean radius in km
}
/**
* Generates boxes for a given route and distance
*
* #param {google.maps.LatLng[] | google.maps.Polyline} path The path along
* which to create boxes. The path object can be either an Array of
* google.maps.LatLng objects or a Maps API v2 or Maps API v3
* google.maps.Polyline object.
* #param {Number} range The distance in kms around the route that the generated
* boxes must cover.
* #return {google.maps.LatLngBounds[]} An array of boxes that covers the whole
* path.
*/
RouteBoxer.prototype.box = function (path, range) {
// Two dimensional array representing the cells in the grid overlaid on the path
this.grid_ = null;
// Array that holds the latitude coordinate of each vertical grid line
this.latGrid_ = [];
// Array that holds the longitude coordinate of each horizontal grid line
this.lngGrid_ = [];
// Array of bounds that cover the whole route formed by merging cells that
// the route intersects first horizontally, and then vertically
this.boxesX_ = [];
// Array of bounds that cover the whole route formed by merging cells that
// the route intersects first vertically, and then horizontally
this.boxesY_ = [];
// The array of LatLngs representing the vertices of the path
var vertices = null;
// If necessary convert the path into an array of LatLng objects
if (path instanceof Array) {
// already an arry of LatLngs (eg. v3 overview_path)
vertices = path;
}
// Build the grid that is overlaid on the route
this.buildGrid_(vertices, range);
// Identify the grid cells that the route intersects
this.findIntersectingCells_(vertices);
// Merge adjacent intersected grid cells (and their neighbours) into two sets
// of bounds, both of which cover them completely
this.mergeIntersectingCells_();
// Return the set of merged bounds that has the fewest elements
return (this.boxesX_.length <= this.boxesY_.length ?
this.boxesX_ :
this.boxesY_);
};
/**
* Generates boxes for a given route and distance
*
* #param {LatLng[]} vertices The vertices of the path over which to lay the grid
* #param {Number} range The spacing of the grid cells.
*/
RouteBoxer.prototype.buildGrid_ = function (vertices, range) {
// Create a LatLngBounds object that contains the whole path
// var routeBounds = new google.maps.LatLngBounds();
var routeBounds = new H.geo.Rect(vertices[0].lat, vertices[0].lng, vertices[0].lat, vertices[0].lng);
// alert(vertices.length);
for (var i = 0; i < vertices.length; i++) {
routeBounds = routeBounds.mergeLatLng(vertices[i].lat, vertices[i].lng);
}
// Find the center of the bounding box of the path
var routeBoundsCenter = routeBounds.getCenter();
// Starting from the center define grid lines outwards vertically until they
// extend beyond the edge of the bounding box by more than one cell
this.latGrid_.push(routeBoundsCenter.lat);
// Add lines from the center out to the north
this.latGrid_.push(routeBoundsCenter.rhumbDestinationPoint(0, range).lat);
for (i = 2; this.latGrid_[i - 2] < routeBounds.getNorthEast().lat; i++) {
this.latGrid_.push(routeBoundsCenter.rhumbDestinationPoint(0, range * i).lat);
}
// Add lines from the center out to the south
for (i = 1; this.latGrid_[1] > routeBounds.getSouthWest().lat; i++) {
this.latGrid_.unshift(routeBoundsCenter.rhumbDestinationPoint(180, range * i).lat);
}
// Starting from the center define grid lines outwards horizontally until they
// extend beyond the edge of the bounding box by more than one cell
this.lngGrid_.push(routeBoundsCenter.lng);
// Add lines from the center out to the east
this.lngGrid_.push(routeBoundsCenter.rhumbDestinationPoint(90, range).lng);
for (i = 2; this.lngGrid_[i - 2] < routeBounds.getNorthEast().lng; i++) {
this.lngGrid_.push(routeBoundsCenter.rhumbDestinationPoint(90, range * i).lng);
}
// Add lines from the center out to the west
for (i = 1; this.lngGrid_[1] > routeBounds.getSouthWest().lng; i++) {
this.lngGrid_.unshift(routeBoundsCenter.rhumbDestinationPoint(270, range * i).lng);
}
// Create a two dimensional array representing this grid
this.grid_ = new Array(this.lngGrid_.length);
for (i = 0; i < this.grid_.length; i++) {
this.grid_[i] = new Array(this.latGrid_.length);
}
};
H.geo.Rect.prototype.getNorthEast = function () {
return new H.geo.Point(this.getTop(), this.getRight());
};
H.geo.Rect.prototype.getSouthWest = function () {
return new H.geo.Point(this.getBottom(), this.getLeft());
};
/**
* Find all of the cells in the overlaid grid that the path intersects
*
* #param {LatLng[]} vertices The vertices of the path
*/
RouteBoxer.prototype.findIntersectingCells_ = function (vertices) {
// Find the cell where the path begins
var hintXY = this.getCellCoords_(vertices[0]);
// Mark that cell and it's neighbours for inclusion in the boxes
this.markCell_(hintXY);
// Work through each vertex on the path identifying which grid cell it is in
for (var i = 1; i < vertices.length; i++) {
// Use the known cell of the previous vertex to help find the cell of this vertex
var gridXY = this.getGridCoordsFromHint_(vertices[i], vertices[i - 1], hintXY);
if (gridXY[0] === hintXY[0] && gridXY[1] === hintXY[1]) {
// This vertex is in the same cell as the previous vertex
// The cell will already have been marked for inclusion in the boxes
continue;
} else if ((Math.abs(hintXY[0] - gridXY[0]) === 1 && hintXY[1] === gridXY[1]) ||
(hintXY[0] === gridXY[0] && Math.abs(hintXY[1] - gridXY[1]) === 1)) {
// This vertex is in a cell that shares an edge with the previous cell
// Mark this cell and it's neighbours for inclusion in the boxes
this.markCell_(gridXY);
} else {
// This vertex is in a cell that does not share an edge with the previous
// cell. This means that the path passes through other cells between
// this vertex and the previous vertex, and we must determine which cells
// it passes through
this.getGridIntersects_(vertices[i - 1], vertices[i], hintXY, gridXY);
}
// Use this cell to find and compare with the next one
hintXY = gridXY;
}
};
/**
* Find the cell a path vertex is in by brute force iteration over the grid
*
* #param {LatLng[]} latlng The latlng of the vertex
* #return {Number[][]} The cell coordinates of this vertex in the grid
*/
RouteBoxer.prototype.getCellCoords_ = function (latlng) {
for (var x = 0; this.lngGrid_[x] < latlng.lng; x++) {}
for (var y = 0; this.latGrid_[y] < latlng.lat; y++) {}
return ([x - 1, y - 1]);
};
/**
* Find the cell a path vertex is in based on the known location of a nearby
* vertex. This saves searching the whole grid when working through vertices
* on the polyline that are likely to be in close proximity to each other.
*
* #param {LatLng[]} latlng The latlng of the vertex to locate in the grid
* #param {LatLng[]} hintlatlng The latlng of the vertex with a known location
* #param {Number[]} hint The cell containing the vertex with a known location
* #return {Number[]} The cell coordinates of the vertex to locate in the grid
*/
RouteBoxer.prototype.getGridCoordsFromHint_ = function (latlng, hintlatlng, hint) {
var x, y;
if (latlng.lng > hintlatlng.lng) {
for (x = hint[0]; this.lngGrid_[x + 1] < latlng.lng; x++) {}
} else {
for (x = hint[0]; this.lngGrid_[x] > latlng.lng; x--) {}
}
if (latlng.lat > hintlatlng.lat) {
for (y = hint[1]; this.latGrid_[y + 1] < latlng.lat; y++) {}
} else {
for (y = hint[1]; this.latGrid_[y] > latlng.lat; y--) {}
}
return ([x, y]);
};
/**
* Identify the grid squares that a path segment between two vertices
* intersects with by:
* 1. Finding the bearing between the start and end of the segment
* 2. Using the delta between the lat of the start and the lat of each
* latGrid boundary to find the distance to each latGrid boundary
* 3. Finding the lng of the intersection of the line with each latGrid
* boundary using the distance to the intersection and bearing of the line
* 4. Determining the x-coord on the grid of the point of intersection
* 5. Filling in all squares between the x-coord of the previous intersection
* (or start) and the current one (or end) at the current y coordinate,
* which is known for the grid line being intersected
*
* #param {LatLng} start The latlng of the vertex at the start of the segment
* #param {LatLng} end The latlng of the vertex at the end of the segment
* #param {Number[]} startXY The cell containing the start vertex
* #param {Number[]} endXY The cell containing the vend vertex
*/
RouteBoxer.prototype.getGridIntersects_ = function (start, end, startXY, endXY) {
var edgePoint, edgeXY, i;
var brng = start.rhumbBearingTo(end); // Step 1.
var hint = start;
var hintXY = startXY;
// Handle a line segment that travels south first
if (end.lat > start.lat) {
// Iterate over the east to west grid lines between the start and end cells
for (i = startXY[1] + 1; i <= endXY[1]; i++) {
// Find the latlng of the point where the path segment intersects with
// this grid line (Step 2 & 3)
edgePoint = this.getGridIntersect_(start, brng, this.latGrid_[i]);
// Find the cell containing this intersect point (Step 4)
edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY);
// Mark every cell the path has crossed between this grid and the start,
// or the previous east to west grid line it crossed (Step 5)
this.fillInGridSquares_(hintXY[0], edgeXY[0], i - 1);
// Use the point where it crossed this grid line as the reference for the
// next iteration
hint = edgePoint;
hintXY = edgeXY;
}
// Mark every cell the path has crossed between the last east to west grid
// line it crossed and the end (Step 5)
this.fillInGridSquares_(hintXY[0], endXY[0], i - 1);
} else {
// Iterate over the east to west grid lines between the start and end cells
for (i = startXY[1]; i > endXY[1]; i--) {
// Find the latlng of the point where the path segment intersects with
// this grid line (Step 2 & 3)
edgePoint = this.getGridIntersect_(start, brng, this.latGrid_[i]);
// Find the cell containing this intersect point (Step 4)
edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY);
// Mark every cell the path has crossed between this grid and the start,
// or the previous east to west grid line it crossed (Step 5)
this.fillInGridSquares_(hintXY[0], edgeXY[0], i);
// Use the point where it crossed this grid line as the reference for the
// next iteration
hint = edgePoint;
hintXY = edgeXY;
}
// Mark every cell the path has crossed between the last east to west grid
// line it crossed and the end (Step 5)
this.fillInGridSquares_(hintXY[0], endXY[0], i);
}
};
/**
* Find the latlng at which a path segment intersects with a given
* line of latitude
*
* #param {LatLng} start The vertex at the start of the path segment
* #param {Number} brng The bearing of the line from start to end
* #param {Number} gridLineLat The latitude of the grid line being intersected
* #return {LatLng} The latlng of the point where the path segment intersects
* the grid line
*/
RouteBoxer.prototype.getGridIntersect_ = function (start, brng, gridLineLat) {
var d = this.R * ((gridLineLat.toRad() - start.lat.toRad()) / Math.cos(brng.toRad()));
return start.rhumbDestinationPoint(brng, d);
};
/**
* Mark all cells in a given row of the grid that lie between two columns
* for inclusion in the boxes
*
* #param {Number} startx The first column to include
* #param {Number} endx The last column to include
* #param {Number} y The row of the cells to include
*/
RouteBoxer.prototype.fillInGridSquares_ = function (startx, endx, y) {
var x;
if (startx < endx) {
for (x = startx; x <= endx; x++) {
this.markCell_([x, y]);
}
} else {
for (x = startx; x >= endx; x--) {
this.markCell_([x, y]);
}
}
};
/**
* Mark a cell and the 8 immediate neighbours for inclusion in the boxes
*
* #param {Number[]} square The cell to mark
*/
RouteBoxer.prototype.markCell_ = function (cell) {
var x = cell[0];
var y = cell[1];
this.grid_[x - 1][y - 1] = 1;
this.grid_[x][y - 1] = 1;
this.grid_[x + 1][y - 1] = 1;
this.grid_[x - 1][y] = 1;
this.grid_[x][y] = 1;
this.grid_[x + 1][y] = 1;
this.grid_[x - 1][y + 1] = 1;
this.grid_[x][y + 1] = 1;
this.grid_[x + 1][y + 1] = 1;
};
/**
* Create two sets of bounding boxes, both of which cover all of the cells that
* have been marked for inclusion.
*
* The first set is created by combining adjacent cells in the same column into
* a set of vertical rectangular boxes, and then combining boxes of the same
* height that are adjacent horizontally.
*
* The second set is created by combining adjacent cells in the same row into
* a set of horizontal rectangular boxes, and then combining boxes of the same
* width that are adjacent vertically.
*
*/
RouteBoxer.prototype.mergeIntersectingCells_ = function () {
var x, y, box;
// The box we are currently expanding with new cells
var currentBox = null;
// Traverse the grid a row at a time
for (y = 0; y < this.grid_[0].length; y++) {
for (x = 0; x < this.grid_.length; x++) {
if (this.grid_[x][y]) {
// This cell is marked for inclusion. If the previous cell in this
// row was also marked for inclusion, merge this cell into it's box.
// Otherwise start a new box.
box = this.getCellBounds_([x, y]);
if (currentBox) {
currentBox = currentBox.mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
} else {
currentBox = box;
}
} else {
// This cell is not marked for inclusion. If the previous cell was
// marked for inclusion, merge it's box with a box that spans the same
// columns from the row below if possible.
this.mergeBoxesY_(currentBox);
currentBox = null;
}
}
// If the last cell was marked for inclusion, merge it's box with a matching
// box from the row below if possible.
this.mergeBoxesY_(currentBox);
currentBox = null;
}
// Traverse the grid a column at a time
for (x = 0; x < this.grid_.length; x++) {
for (y = 0; y < this.grid_[0].length; y++) {
if (this.grid_[x][y]) {
// This cell is marked for inclusion. If the previous cell in this
// column was also marked for inclusion, merge this cell into it's box.
// Otherwise start a new box.
if (currentBox) {
box = this.getCellBounds_([x, y]);
currentBox = currentBox.mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
} else {
currentBox = this.getCellBounds_([x, y]);
}
} else {
// This cell is not marked for inclusion. If the previous cell was
// marked for inclusion, merge it's box with a box that spans the same
// rows from the column to the left if possible.
this.mergeBoxesX_(currentBox);
currentBox = null;
}
}
// If the last cell was marked for inclusion, merge it's box with a matching
// box from the column to the left if possible.
this.mergeBoxesX_(currentBox);
currentBox = null;
}
};
/**
* Search for an existing box in an adjacent row to the given box that spans the
* same set of columns and if one is found merge the given box into it. If one
* is not found, append this box to the list of existing boxes.
*
* #param {LatLngBounds} The box to merge
*/
RouteBoxer.prototype.mergeBoxesX_ = function (box) {
if (box !== null) {
for (var i = 0; i < this.boxesX_.length; i++) {
if (this.boxesX_[i].getNorthEast().lng === box.getSouthWest().lng &&
this.boxesX_[i].getSouthWest().lat === box.getSouthWest().lat &&
this.boxesX_[i].getNorthEast().lat === box.getNorthEast().lat) {
this.boxesX_[i] = this.boxesX_[i].mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
return;
}
}
this.boxesX_.push(box);
}
};
/**
* Search for an existing box in an adjacent column to the given box that spans
* the same set of rows and if one is found merge the given box into it. If one
* is not found, append this box to the list of existing boxes.
*
* #param {LatLngBounds} The box to merge
*/
RouteBoxer.prototype.mergeBoxesY_ = function (box) {
if (box !== null) {
for (var i = 0; i < this.boxesY_.length; i++) {
if (this.boxesY_[i].getNorthEast().lat === box.getSouthWest().lat &&
this.boxesY_[i].getSouthWest().lng === box.getSouthWest().lng &&
this.boxesY_[i].getNorthEast().lng === box.getNorthEast().lng) {
this.boxesY_[i] = this.boxesY_[i].mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
return;
}
}
this.boxesY_.push(box);
}
};
/**
* Obtain the LatLng of the origin of a cell on the grid
*
* #param {Number[]} cell The cell to lookup.
* #return {LatLng} The latlng of the origin of the cell.
*/
RouteBoxer.prototype.getCellBounds_ = function (cell) {
return new H.geo.Rect(this.latGrid_[cell[1]+1], this.lngGrid_[cell[0]],this.latGrid_[cell[1]], this.lngGrid_[cell[0] + 1]);
};
/* Based on the Latitude/longitude spherical geodesy formulae & scripts
at http://www.movable-type.co.uk/scripts/latlong.html
(c) Chris Veness 2002-2010
*/
H.geo.Point.prototype.rhumbDestinationPoint = function (brng, dist) {
var R = 6371; // earth's mean radius in km
var d = parseFloat(dist) / R; // d = angular distance covered on earth's surface
var lat1 = this.lat.toRad(), lon1 = this.lng.toRad();
brng = brng.toRad();
var lat2 = lat1 + d * Math.cos(brng);
var dLat = lat2 - lat1;
var dPhi = Math.log(Math.tan(lat2 / 2 + Math.PI / 4) / Math.tan(lat1 / 2 + Math.PI / 4));
var q = (Math.abs(dLat) > 1e-10) ? dLat / dPhi : Math.cos(lat1);
var dLon = d * Math.sin(brng) / q;
// check for going past the pole
if (Math.abs(lat2) > Math.PI / 2) {
lat2 = lat2 > 0 ? Math.PI - lat2 : - (Math.PI - lat2);
}
var lon2 = (lon1 + dLon + Math.PI) % (2 * Math.PI) - Math.PI;
if (isNaN(lat2) || isNaN(lon2)) {
return null;
}
return new H.geo.Point(lat2.toDeg(), lon2.toDeg());
};
H.geo.Point.prototype.rhumbBearingTo = function (dest) {
var dLon = (dest.lng - this.lng).toRad();
var dPhi = Math.log(Math.tan(dest.lat.toRad() / 2 + Math.PI / 4) / Math.tan(this.lat.toRad() / 2 + Math.PI / 4));
if (Math.abs(dLon) > Math.PI) {
dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
}
return Math.atan2(dLon, dPhi).toBrng();
};
/**
* Extend the Number object to convert degrees to radians
*
* #return {Number} Bearing in radians
* #ignore
*/
Number.prototype.toRad = function () {
return this * Math.PI / 180;
};
/**
* Extend the Number object to convert radians to degrees
*
* #return {Number} Bearing in degrees
* #ignore
*/
Number.prototype.toDeg = function () {
return this * 180 / Math.PI;
};
/**
* Normalize a heading in degrees to between 0 and +360
*
* #return {Number} Return
* #ignore
*/
Number.prototype.toBrng = function () {
return (this.toDeg() + 360) % 360;
};

How to run a geo "nearby" query with firestore?

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?
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)
}
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);
}
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
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.
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.
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.
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
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!
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.
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
}
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.
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());
})
});
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
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)

Resources