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.
Related
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'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.
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.
I am trying to delete all old coordinates from an array from the condition that the coordinates are too far away from my current location. However the repeat statement gets stuck on repeat, deleting all the coordinates.
var locationArray = [Double]()
var distArray = [CLLocationDistance]()
let maxDis: CLLocationDistance = CLLocationDistance(exactly: 2000)!
let LAT = Double(location.coordinate.latitude)
let LONG = Double(location.coordinate.longitude)
repeat{
locationArray.insert(contentsOf: [LAT, LONG], at: 0)
} while locationArray.count <= 4
let oldCo = CLLocation(latitude: LAT, longitude: LONG)
let newlat = locationArray[2]
let newlong = locationArray[3]
let newCo = CLLocation(latitude: newlat, longitude: newlong)
let dist = newCo.distance(from: oldCo)
distArray.append(dist)
let distArraySum = (distArray.reduce(0) { $0 + $1 })
print(distArraySum)
repeat{
if distArraySum >= maxDis {
locationArray.remove(at: locationArray.count-2)
locationArray.remove(at: locationArray.count-1)
distArray.remove(at: 0)
print("deleted Coordinates")
}
} while distArraySum >= maxDis
The app terminates because I am getting stuck on my repeat statement, and every element in the distArray gets removed until there are no more elements. So when it repeats again and I try to remove an element from an empty array, I get a fatal error.
Once you calculate disArraySum you never recalculate it within the repeat loop. The result of distArraySum >= maxDis will never change and so your loop will either always execute or never execute, but it will never dynamically change state.
Additionally, you might want to consider changing locationArray to an array of tuples; it looks like you're interleaving lat and lon, it would be easier conceptually e.g.:
var locationArray:[(lat: Double, lon: Double)] = []
This will let you avoid having to do
locationArray.remove(at: locationArray.count-2)
locationArray.remove(at: locationArray.count-1)
And while we're at it, if I can make a suggestion to use for each instead, then the final loop might look like the following untested code:
for (i, coordinates) in locationArray.enumerate().reverse() {
if (some condition using coordinates.lat and coordinates.lon) {
locationArray.removeAtIndex(i)
}
}
I am trying to see whether the user is within a certain distance of an address. I have successfully managed to get the users location, and convert the address with forward geocoding. I am left with two sets of coordinates. I am trying to make an if statement saying if they are within "a distance", print something!
Currently when i print the coordinates inside the placemark function i get the desired coordinates. When i call them to create eventLatitude and eventLongitude they become 0.0. I know this is a ascycronous problem, but i am unsure on who to resolve this. Can someone give me an example.
My code is below
before the viewdidload i have these variables
var placemarkLongitude = CLLocationDegrees()
var placemarkLatitude = CLLocationDegrees()
then inside the function i set these variables to the placemark coordinates
if let objects = objects {
for object in objects {
self.geocoder = CLGeocoder()
//get address from object
let COAddress = object.objectForKey("Address")as! String
let COCity = object.objectForKey("City")as! String
let COState = object.objectForKey("State")as! String
let COZipCode = object.objectForKey("ZipCode")as! String
let combinedAddress = "\(COAddress) \(COCity) \(COState) \(COZipCode)" //all parts of address
print(combinedAddress)
//make address a location
self.geocoder.geocodeAddressString(combinedAddress, completionHandler: {(placemarks, error) -> Void in
if(error != nil)
{
print("Error", error)
}
else if let placemark = placemarks?[0]
{
let placemark = placemarks![0]
self.placemarkLatitude = (placemark.location?.coordinate.latitude)! //THIS RETURNS A VALUE
self.placemarkLongitude = (placemark.location?.coordinate.longitude)! //THIS RETURNS A VALUE
print("Longitude: ", self.placemarkLongitude, " Latitude: ", self.placemarkLatitude)
}
})
// user location
let userLatitude = self.locationManager.location?.coordinate.latitude //THIS RETURNS A VALUE
let userLongitude = self.locationManager.location?.coordinate.longitude //THIS RETURNS A VALUE
print("User Location is ", userLatitude, ", " ,userLongitude)
let userLocation = CLLocation(latitude: userLatitude!, longitude: userLongitude!)
// event location
let eventLatitude = self.placemarkLatitude // THIS RETURNS 0.0
let eventLongitude = self.placemarkLatitude // THIS RETURNS 0.0
print("Event Location is ", eventLatitude, ", " ,eventLongitude)
let eventLocation = CLLocation(latitude: eventLatitude, longitude: eventLongitude)
//Measuring my distance to my buddy's (in km)
let distance = userLocation.distanceFromLocation(eventLocation) / 1000
//Display the result in km
print("The distance to event is ", distance)
if (distance < 100) {
print("yay")
}
}
}
You are correct about the asynchronous issue. Basically, you cannot do anything after this code:
// [A1]
self.geocoder.geocodeAddressString(combinedAddress, completionHandler: {
(placemarks, error) -> Void in
// [B] ... put everything _here_
})
// [A2] ... nothing _here_
The reason is that the stuff inside the curly braces (B) happens later than the stuff outside it (including the stuff afterward, A2). In other words, the code in my schematic above runs in the order A1, A2, B. But you are dependent on what happens inside the curly braces, so you need that dependent code to be inside the curly braces so that it executes in sequence with the results of the geocoding.
Of course this also means that the surrounding function cannot return a result, because it returns before the stuff in curly braces has even happened. The code in my schematic goes A1, A2, return! Only later does B happen. So clearly you cannot return anything that happens in B because it hasn't happened yet.
Just pass the coordinate values obtained from the completionHandler to any other method and do what you like to do.
{
self.placemarkLatitude = (placemark.location?.coordinate.latitude)! //THIS RETURNS A VALUE
self.placemarkLongitude = (placemark.location?.coordinate.longitude)! //THIS RETURNS A VALUE
// After this code pass the values like,
passingTheCoordinates(placemarkLatitude, placemarkLongitude)
}
func passingTheCoordinates(latitude:CLLocationDegrees, _ longitude:CLLocationDegrees){
}
Did not have enough reputation to reply your question but I also have this same problem today. I don't know much about your app design but for my case (which is stuck at the same place like you, same func, same problem, can't save to variable). My solution (maybe kinda temporally, does not good) is to save (placemark.location?.coordinate.latitude)! and (placemark.location?.coordinate.longitude)! to CoreData as Double.
This is how I implemented it. As I said before, since I don't know your app much so depend on your need, you might want something else.
LocationManager.sharedInstance.getReverseGeoCodedLocation(address: searchBar.text!, completionHandler: { (location:CLLocation?, placemark:CLPlacemark?, error:NSError?) in
if error != nil {
print((error?.localizedDescription)!)
return
}
if placemark == nil {
print("Location can't be fetched")
return
}
//Saving geo code to Core Data
newEntry.lat = (placemark?.location?.coordinate.latitude)!
newEntry.long = (placemark?.location?.coordinate.longitude)!
})
Credit to this repo for the LocationManager.swift file