I'm using Firebase with GeoFire 3.0 Cocoapod in my Swift App to populate a map with markers all over the world. Here is the code to perform the circle query to get the markers within the area currently displayed on the map:
dbRef = Database.database().reference()
let geoRef = GeoFire(firebaseRef: dbRef.child("markers"))
let center = CLLocation(latitude: currentLocation.latitude, longitude: currentLocation.longitude)
print("Center: "," Lat: ",currentLocation.latitude," Long: ",currentLocation.longitude )
let circleQuery = geoRef.query(at: center, withRadius: 100)
circleQuery.observe(.keyEntered, with: { key, location in
print("key: ",key,"Location: ",location)
let markerKey = key
let markerLat = location.coordinate.latitude
let markerLong = location.coordinate.longitude
//read "verified" flag from firebase record "key"
self.dbRef.child(markerKey).observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
let verified = value?["verified"] as? Bool
print("key: ",key,"Location: ",location,"verified: ",verified as Any)
...
})
})
When the circle query is expanded by the user zooming out to display the map of the entire world (radius of 8000 Km (4791 MI)), the query aborts with an NSException.
The Xcode debugger shows GeoFire has calculated a latitude of 105.9793939... and longitude of -112.05707936...
Geofire should restrict the latitude at +/- 90 and the longitude at +/- 180 and in that case all GeoFire data should be returned from the Query.
Here is a screenshot of the error in Xcode:
Xcode Error Screenshot
Has anyone else seen this issue and/or found a solution?
Since GeoFire clearly does not limit the latitude and longitude in the way you want it to, you have two options
Report an issue on the GeoFire repo, and possibly propose a PR yourself.
Limit the values to the ranges you want in your own application code.
I'd suggest going with the second approach, since you'll want/need to show that you clipped/restricted the range in the UI anyway.
Related
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: coord.latitude, longitude: coord.longitude)) { [weak self] placeMarks, error in
print("PLACEMARKSFIRST:")
print(placeMarks?.first)
self?.geoCodeCounter += 1
guard let place = placeMarks?.first else {
self?.selectionSubject.accept(nil)
self?.geoCodeAddress()
return
}
print("PLACEMARKSFIRST:")
print(placeMarks?.first)
self?.geoCodeCounter += 1
guard let place = placeMarks?.first else {
self?.selectionSubject.accept(nil)
self?.geoCodeAddress()
return
}
print("PLACE:")
print(place)
print(place.administrativeArea)
print(place.subAdministrativeArea)
print(place.locality)
print(place.subLocality)
print(place.thoroughfare)
print(place.subThoroughfare)
self?.geoAdministrativeArea = place.administrativeArea ?? ""
self?.geoSubAdministrativeArea = place.subAdministrativeArea ?? ""
self?.geoLocality = place.locality ?? ""
self?.geoSubLocality = place.subLocality ?? ""
self?.geoThoroughfare = place.thoroughfare ?? ""
self?.geoSubThoroughfare = place.subThoroughfare ?? ""
DEBUG CODES:
PLACE: Darüşşafaka Cd. 5/2, Darüşşafaka Cd. 5/2, 34457 Sarıyer İstanbul, Türkiye # <+41.12570667,+29.02811721> +/- 100.00m, region CLCircularRegion (identifier:'<+41.12570100,+29.02817000> radius 70.68', center:<+41.12570100,+29.02817000>, radius:70.68m)
Optional("İstanbul")
Optional("Sarıyer")
Optional("Sarıyer")
Optional("Çamlıtepe")
Optional("Darüşşafaka Cd.")
Optional("5/2")
As you see above there is a conflict between the locality inside place and the response that place.locality returns. Locality must be "Darüşşafaka". How can I fix this?
Is it about a bug in CLGeocoder library?
Actually its not a bug even its not a mistake that you can fix.
You get what the locality value is at the location specified on the Apple maps database
If you see the doc you will see :
If the placemark location is Apple’s headquarters, for example, the value for this property would be the string “Cupertino”.
The locality value of Apple headquarters is Cupertino because it is expressed that way on maps.
As a result, if the locality value is defined as Sarıyer in the coordinates you entered, there is no way to change it.
You may need to check and manipulate other properties to get the value you want
The code that retrieves all locations from Firestore within a 50km location of a given point is given on the Firebase website. Here it is:
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInM: Double = 50 * 1000
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
return db.collection("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
if distance <= radiusInM {
matchingDocs.append(document)
}
}
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
The issue that I am having is that matchingDocs (the empty array that the locations from the database are supposed to append to) returns empty every time.
I have double checked that the center and example locations in my database are within 50km of each other. The code is able to retrieve the four documents in my database and I know this because if I put a print statement in the last for loop, I get something printed 4 times.
I need an explanation on exactly what the getDocumentsCompletion function does because I don't understand fully what it does, or how the call works. There are no arguments passed in where the function is called.
I have also added print statements within the getDocumentsCompletion function, but nothing ever gets printed out, so I believe that my issue lies there. I would like an explanation on what exactly is going on there so I can better address the issue.
I am using the code given by Firebase for the Geo Query feature which I found from this post here: GeoHash Queries in Firebase with Swift
Right now, I am calling this function in the init of one of my classes, and I need this query to be completed before the rest of my code runs. I believe this is wait async/await comes into play but I am new to this concept and I am having trouble implementing this logic. In the code's current state, my attempt to print the number of Docs at the end will print 0, because that last print statement runs before my query completes. How do I make the code after my query wait until my query has completed?
Example format of the data in Firebase:
geohash:"9mupwu3mkc"
id:"13101C7F-D7FF-4141-BC5A-76602173C096"
lat:33.6863622
lng:-117.8264411
Address:"1 Civic Center Plaza, Irvine CA 92606"
Here is the code:
func getallDocs(radius: Double) {
// Find pickups within 50km of Basecamp
let center = CLLocationCoordinate2D(latitude: 33.9742268, longitude: -118.3947792)
let radiusInKilometers: Double = radius
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInKilometers)
let queries = queryBounds.compactMap { (any) -> Query? in
guard let bound = any as? GFGeoQueryBounds else { return nil }
return db.collection("pickups")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
print("\nDocs: Count \(documents.count)")
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let ownerAddress = document.data()["ownerAddress"] as? String ?? "no address"
let coordinates = CLLocation(latitude: lat, longitude: lng)
let centerPoint = CLLocation(latitude: center.latitude, longitude: center.longitude)
// We have to filter out a few false positives due to GeoHash accuracy, but
// most will match
let distance = GFUtils.distance(from: centerPoint, to: coordinates)
print("ownerAddress: \(ownerAddress), distance: \(distance) \tlat: \(lat), \(lng)")
if distance <= radiusInKilometers {
matchingDocs.append(document)
}
}
}
// After all callbacks have executed, matchingDocs contains the result. Note that this
// sample does not demonstrate how to wait on all callbacks to complete.
for query in queries {
query.getDocuments(completion: getDocumentsCompletion)
}
print("Docs: \(matchingDocs.count)")
}
I want to find nearest users to my location. (For example up to 5km) My nodes in firebase database like below structure,
+ users
+ user_id0
- latitude: _
- longitude: _
Is there any way getting exact users in that radius. Otherwise I should check each user of them nearest position or not to me using CLLocation distance method.
I'd highly recommend using Geofire for something like this.
To set it up, your data structure will slightly change. You can still store lat/lng on your users, but you will also create a new Firebase table called something like users_locations
Your users_locations table will be populated through Geofire, and will look something like this
users_locations
user_id0:
g: 5pf666y
l:
0: 40.00000
1: -74.00000
In general, this is how you would store a location in Geofire, but you can set it up to save whenever your user object is created / updates location.
let geofireRef = FIRDatabase.database().reference().child("users_locations")
let geoFire = GeoFire(firebaseRef: geofireRef)
geoFire.setLocation(CLLocation(latitude: lat, longitude: lng), forKey: "user_id0")
When you've saved your locations in users_locations, you can then use a GFQuery to query for all the users in a certain range.
let center = CLLocation(latitude: yourLat, longitude: yourLong)
var circleQuery = geoFire.queryAtLocation(center, withRadius: 5)
var queryHandle = circleQuery.observeEventType(.KeyEntered, withBlock: { (key: String!, location: CLLocation!) in
println("Key '\(key)' entered the search area and is at location '\(location)'")
})
I want to know how to layout a Firebase database so I can easily get Data about coordinates. (Only started using Firebase yesterday so am very very new to it)
ViewController
var reference: DatabaseReference!
var handle: DatabaseHandle?
viewDidLoad
reference = Database.database().reference()
handle = ref.child("Places/America/Capital/Washington/Coordinates").observe(.value, with: { (snapshot) in
for child in snapshot.children {
let theSnap = child as! DataSnapshot
let theValue = theSnap.value as! Double
print(theValue)
}
This prints the coordinates perfectly. But is there any way I can get all the coordinates in my Database? Am I laying out the Database wrong or what syntax should I use in order to get all data that has latitude and longitude in it?
Image of Firebase Database
edit 1
Is there no easy way of doing a for-in loop instead of just copying the handle line and changing the necessary file path?
You are so very close.
The path you specified (the Firebase Reference) is for a specific set of coordinates at a coordinate node. You then iterate over the two child nodes within Coordinates which prints the latitude and longitude child nodes.
If you want to get the coords for all, you need to go up a few levels and use the same technique to iterate over the child nodes.
It's a bit hard to wrap your brain around at first so let me give you a leg up. Here's the complete code and the output. Note that I stored the coords as int's so adjust accordingly. I would probably suggest storing them as strings.
let placesRef = self.ref.child("Places")
placesRef.observeSingleEvent(of: .value, with: { snapshot in
for countryChild in snapshot.children { //iterate over each country
let countryChildSnap = countryChild as! DataSnapshot //cast the child to a snapshot
let countryName = countryChildSnap.key //get it's key, the country name
print("country: \(countryName)")
let capitalSnap = countryChildSnap.childSnapshot(forPath: "Capital") //get the nodes within the capital node
for stateChild in capitalSnap.children { //iterate over each state within the country
let stateChildSnap = stateChild as! DataSnapshot
let stateName = stateChildSnap.key
print(" state: \(stateName)")
let coordsSnap = stateChildSnap.childSnapshot(forPath: "Coordinates")
let lat = coordsSnap.childSnapshot(forPath: "latitude").value as! Int
let lon = coordsSnap.childSnapshot(forPath: "longitude").value as! Int
print(" latitude: \(lat) longitude: \(lon)")
}
}
})
and the output
country: America
state: Washington
latitude: 38 longitude: -77
country: France
state: Paris
latitude: 48 longitude: 2
country: Germany
state: Berlin
latitude: 52 longitude: 13
Note that I only want to read this once so I am using .observeSingleEvent. If you want to be notified of future changes, leverage .observe with .childAdded, .childChanged and .childRemoved.