Mapkit, how to change annotation coordinates to nearest address? - ios

I have a navigation application I am working on, and one use of it is that it can calculate the average of all the annotations coordinates placed by the user(through a search table, and each annotation is placed when they press a result) and find what you might call a middle point, in between all the annotations. This midpoint, however, only goes by coordinates at the moment, meaning that depending on where the users current annotations are, this mid point could wind up in the middle of a lake or a forest, which is not helpful. I want it to find the nearest address to the coordinates of my middle point, and redirect the annotation to there instead. Here's how the annotation is created:
#IBAction func middleFinderButton(_ sender: Any) {
let totalLatitude = mapView.annotations.reduce(0) { $0 + $1.coordinate.latitude }
let totalLongitude = mapView.annotations.reduce(0) { $0 + $1.coordinate.longitude }
let averageLatitude = totalLatitude/Double(mapView.annotations.count)
let averageLongitude = totalLongitude/Double(mapView.annotations.count)
let centerPoint = MKPointAnnotation()
centerPoint.coordinate.latitude = averageLatitude
centerPoint.coordinate.longitude = averageLongitude
mapView.addAnnotation(centerPoint)
}
How can I get this annotation 'centerPoint' to adjust to the nearest address? Thanks.

I would just use a reverse geocode here returning an MKPlacemark. The documentation suggests that normally just one placemark will be returned by the completion handler, on the main thread, so you can use the result straightaway to update the UI. MKPlacemark conforms to the annotation protocol so you can put it directly on the map:
func resolveAddress(for averageCoordinate: CLLocationCoordinate2D, completion: #escaping (MKPlacemark?) -> () ) {
let geocoder = CLGeocoder()
let averageLocation = CLLocation(latitude: averageCoordinate.latitude, longitude: averageCoordinate.longitude)
geocoder.reverseGeocodeLocation(averageLocation) { (placemarks, error) in
guard error == nil,
let placemark = placemarks?.first
else {
completion(nil)
return
}
completion(MKPlacemark(placemark: placemark))
}
}
#IBAction func middleFinderButton(_ sender: Any) {
// your code to find center annotation
resolveAddress(for: centerPoint.coordinate) { placemark in
if let placemark = placemark {
self.mapView.addAnnotation(placemark)
} else {
self.mapView.addAnnotation(centerCoordinate)
}
}

Related

Why does a GeoFire query sometimes use data from a previous load?

So sometimes someone in entered the search radius is from before, ie someone who was in search radius, but based on the current data in the database is not in the radius. Other times, someone who wasn't in the search radius before but now is, doesn't get printed.
This only happens once each time, ie if I load the app for the second time after the erroneous inclusion or exclusion, the correct array prints.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let databaseRef = Database.database().reference()
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
print("locations = \(locValue.latitude) \(locValue.longitude)")
latestLocation = ["latitude" : locValue.latitude, "longitude" : locValue.longitude]
let lat = locValue.latitude
let lon = locValue.longitude
dict = CLLocation(latitude: lat, longitude: lon)
print("dict", dict)
if let locationDictionary = latestLocation {
databaseRef.child("people").child(uid).child("Coordinates").setValue(locationDictionary)
let geofireRef = Database.database().reference().child("Loc")
let geoFire = GeoFire(firebaseRef: geofireRef)
print(CLLocation(latitude: lat, longitude: lon),"GGG")
geoFire.setLocation(CLLocation(latitude: lat, longitude: lon), forKey: uid)
}
manager.stopUpdatingLocation()
}
Override func ViewdidLoad() {
super.viewDidLoad()
guard let uid = Auth.auth().currentUser?.uid else { return }
let geofireRef = Database.database().reference().child("Loc")
let geoFire = GeoFire(firebaseRef: geofireRef)
geoFire.getLocationForKey(uid) { (location, error) in
if (error != nil) {
print("An error occurred getting the location for \"Coordinates\": \(String(describing: error?.localizedDescription))")
} else if (location != nil) {
print("Location for \"Coordinates\" is [\(location?.coordinate.latitude), \(String(describing: location?.coordinate.longitude))]")
} else {
print("GeoFire does not contain a location for \"Coordinates\"")
}
}
let query1 = geoFire.query(at: self.dict, withRadius: 3)
query1.observe(.keyEntered, with: { key, location in
print("Key: " + key + "entered the search radius.") ///**this prints keys of users within 3 miles. This is where I see the wrong inclusions or exclusions**
do {
self.componentArray.append(key)
}
print(self.componentArray,"kr")
}
)
}
Here's what I would do for testing and maybe a solution. This is similar to your code but takes some of the unknowns out of the equation; I think we maybe running into an asynchronous issue as well, so give this a try.
In viewDidLoad get the current users position. That position will be used as the center point of the query
self.geoFire.getLocationForKey(uid) { (location, error) in
if (error != nil) {
print("An error occurred getting the location for \"Coordinates\": \(String(describing: error?.localizedDescription))")
} else if (location != nil) {
self.setupCircleQueryWith(center: location) //pass the known location
} else {
print("GeoFire does not contain a location for \"Coordinates\"")
}
}
Once the location var is populated within the closure (so you know it's valid) pass it to a function to generate the query
func setupCircleQueryWith(center: CLLLocation) {
var circleQuery = self.geoFire.queryAtLocation(center, withRadius: 3.0)
self.queryHandle = self.circleQuery.observe(.keyEntered, with: { key, location in
print("Key '\(key)' entered the search area and is at location '\(location)'")
self.myKeyArray.append(key)
})
}
self.queryHandle is a class var we can use to remove the query at a later time. I also set up self.geoFire as a class var that points to Loc.
EDIT
At the very top of your class, add a class var to store the keys
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
var ref: DatabaseReference!
var myKeyArray = [String]()
let queryHandle: DatabaseHandle!
and remember to also add a .keyExited event so you will know when to remove a key from the array when the key exits the area.

Trying to use reverseGeocodeLocation, but completionHandler code is not being executing

The issue is that the code inside the completionHandler block is never run; I used breakpoints and the program would skip over the completion handler block in build mode
Below are two functions used within PinALandPlaceMark, where most of my code is located
func generateRandomWorldLatitude()-> Double{
let latitude = Double.random(in: -33 ..< 60)
return latitude
}
func generateRandomWorldLongitude()-> Double{
let longitude = Double.random(in: -180 ..< 180)
return longitude
}
func PinALandPlaceMark() -> MKAnnotation {
var LandBasedCountryHasYetToBeFound : (Bool, CLLocationDegrees?, CLLocationDegrees?)
LandBasedCountryHasYetToBeFound = (false,nil,nil)
let randomPinLocation = MKPointAnnotation()
repeat{
if LandBasedCountryHasYetToBeFound == (false,nil,nil){
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(randomCoordinate, completionHandler: { (placemarks, error) -> Void in
if error != nil{print(error)}else{
guard let placemark = placemarks?.first else{return}
//check which placemark property exists, store in the 0 alpha labelForClosure
if let countryExists = placemark.country {
LandBasedCountryHasYetToBeFound = (true,latitude,longitude)
//country = countryExists as String
// viewController.labelForClosure.text = countryExists
print(" Country Exists!: \(countryExists)")
print(" randomCoordinate \(randomCoordinate)")
}
}
})
}
// print("The country found was on land. This statement is \(LandBasedCountryHasYetToBeFound.occursInCountry)")
else{
let coordinatesOfrandomPinLocation = CLLocationCoordinate2D(latitude: LandBasedCountryHasYetToBeFound.1!, longitude: LandBasedCountryHasYetToBeFound.2!)
randomPinLocation.title = ""//LandBasedCountryHasYetToBeFound.countryName
randomPinLocation.coordinate = coordinatesOfrandomPinLocation
// viewController.mapView.addAnnotation(randomPinLocation)
}
}while LandBasedCountryHasYetToBeFound.0 == false
print("randomPin has been returned, now use pin function inside placemark declaration")
return randomPinLocation
}
Your main problem is that your CLGeocoder instance is held in a local variable inside the loop; This means that it will be released before it has completed its task.
You have a couple of other issues too which would cause you problems even if the reverse geo-coding did complete.
The main one is that you are checking for loop termination using a boolean that is set inside the closure; The closure will execute asynchronously, so the loop will have executed many more times before the boolean is set to true in the case where an address is found.
The second problem is related to and made worse by this; reverse geocoding is rate limited. If you submit too many requests too quickly, Apple's servers will simply return an error. Even if you did wait for the first response before submitting a second, your chances of hitting land at random are pretty low, so you will probably hit this limit pretty quickly.
Ignoring the rate limit problem for the moment, you can use a recursive function that accepts a completion handler rather than using a loop and trying to return a value.
var geoCoder = CLGeocoder()
func pinALandPlaceMark(completion: #escaping (Result<MKAnnotation, Error>) -> Void) {
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
geocoder.reverseGeocodeLocation(randomCoordinate) { (placemarks, error) in
guard error == nil else {
completion(nil,error)
return error
}
if let placemark = placemarks.first, let _ = placemark.country {
let randomPinLocation = MKPointAnnotation()
randomPinLocation.coordinate = randomCoordinate.coordinate
completionHandler(randomPinLocation,nil)
} else {
pinALandPlaceMark(completion:completion)
}
}
}
The first thing we do is declare a property to hold the CLGeocoder instance so that it isn't released.
Next, this code checks to see if a placemark with a country was returned. If not then the function calls itself, passing the same completion handler, to try again. If an error occurs then the completion handler is called, passing the error
To use it, you would say something like this:
pinALandPlaceMark() { result in
switch result {
case .success(let placemark):
print("Found \(placemark)")
case .failure(let error):
print("An error occurred: \(error)")
}
}

Mapbox Navigation in iOS with in my mapView controller

I want to integrate Mapbox navigation in iOS, I can easily get the direction/route between two coordinate also to get the navigation path from mapbox we can use below code
let options = NavigationOptions(styles: nil)
let viewController = NavigationViewController(for: self.directionsRoute!)
viewController.delegate=self
self.present(viewController, animated: true, completion: nil)
But the problem is I want to display the navigation in my mapview which is a part of another view controller, I can do that by getting a direction/route and instruction but I can't find any method which will be called every second so that I can update route instruction, as well as route, in case of user change the path.
Let me know if I am missing anything or any changes needed.
-Thanks in advance
here is my approach:
first i did get only directions instructions from the MapBox api taking advantage of it's free API calls quota and draw the instructions on GMSMapView or MapKit taking advantage of their good performance and memory management.
podfile
pod 'MapboxDirections.swift'
import MapboxDirections
this is done through the below code
have the property for MapBox directions
#IBOutlet weak var googleMapView: GMSMapView!
let locationManager = CLLocationManager()
let mapBoxirections = Directions(accessToken: osmToken)
var path: GMSMutablePath?
then do the actual api call
private func drawRouteBetween(source: StopModel, destination: StopModel) {
guard let name = source.name, let lat = source.latitude, let lng = source.longitude else { return }
guard let nameDest = destination.name, let latDest = destination.latitude, let lngDest = destination.longitude else { return }
let waypoints = [
Waypoint(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lng), name: name),
Waypoint(coordinate: CLLocationCoordinate2D(latitude: latDest, longitude: lngDest), name: nameDest),
]
let options = RouteOptions(waypoints: waypoints, profileIdentifier: .automobile)
options.includesSteps = true
options.distanceMeasurementSystem = .metric
mapBoxirections.calculate(options) { (waypoints, routes, error) in
guard error == nil else {
print("Error calculating directions: \(error!)")
return
}
if let route = routes?.first, let leg = route.legs.first {
for step in leg.steps {
if let coordinates = step.coordinates {
for (index, point) in coordinates.enumerated() {
let source = point
if index <= coordinates.count - 2 {
let destination = coordinates[index + 1]
self.drawPolyLine(source: source, destination: destination)
}
}
}
}
}
}
}
note that StopModel is my custom made CLLocation so feel free to replace it with your own as long it has the latitude and longitude
create the method that draws Polyline on your CLLocationManagerDelegate as below
private func drawPolyLine(source: CLLocationCoordinate2D, destination: CLLocationCoordinate2D){
path?.add(source)
path?.add(destination)
let polyLine = GMSPolyline(path: path)
polyLine.strokeWidth = 4 // width of your choice
polyLine.strokeColor = .red // color of your choice
polyLine.map = googleMapView
}
then take a look at the MapBoxDirections.Route model and explore it's properties you will find very useful info inside it
and then take advantage of the callback function from the GMS Delegate that notifies you with the location update instead having a timer and calling it every second this is more efficient way
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
/* do your business here */
}
do not forget to have the delegate of the location manager to self or the class of your choice
Maybe this helps a bit: you can easily add observer for route progress changes:
NotificationCenter.default.addObserver(self,
selector: #selector(progressDidChange(notification:)),
name: .routeControllerProgressDidChange,
object: navigationService.router)
You need a navigation service with your route by creating it like
let navigationService = MapboxNavigationService(route: route)
The function progressDidChange can do something like:
#objc func progressDidChange(notification: NSNotification) {
guard let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress,
let location = notification.userInfo?[RouteControllerNotificationUserInfoKey.locationKey] as? CLLocation else {
return
}
// you have all information you probably need in routeProgress, f.E.
let secondsRemaining = routeProgress.currentLegProgress.currentStepProgress.durationRemaining
...
}

How to store drop pin destination coordinates swift

I'm trying to store the drop pin destination coordinates and pass them back through the delegate? I have the below drop pin function.
func dropPinFor(placemark: MKPlacemark) {
selectedItemPlacemark = placemark
for annotation in mapView.annotations {
if annotation.isKind(of: MKPointAnnotation.self) {
// mapView.removeAnnotation(annotation) // removing the pins from the map
}
}
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.coordinate
mapView.addAnnotation(annotation)
let (destLat, destLong) = (placemark.coordinate.latitude, placemark.coordinate.longitude)
print("This is the pins destinations coord \(destLat, destLong)")
}
But when I try to print before sending data back through delegate it print's 0.0 lat 0.0 long
#IBAction func addBtnWasPressed(_ sender: Any) {
if delegate != nil {
if firstLineAddressTextField.text != "" && cityLineAddressTextField.text != "" && postcodeLineAddressTextField.text != "" {
//Create Model object DeliveryDestinations
let addressObj = DeliveryDestinations(NameOrBusiness: nameOrBusinessTextField.text, FirstLineAddress: firstLineAddressTextField.text, SecondLineAddress: countryLineAddressTextField.text, CityLineAddress: cityLineAddressTextField.text, PostCodeLineAddress: postcodeLineAddressTextField.text, DistanceToDestination: distance, Lat: destlat, Long: destlong)
print(distance)
print("This is the latitude to use with protocol \(destlat)")
print("This is the latitude to use with protocol \(destlong)")
//add that object to previous view with delegate
delegate?.userDidEnterData(addressObj: addressObj)
//Dismising VC
//navigationController?.popViewController(animated: true)
clearTextFields()
}
}
}
You are declaring the (destLat, destLong) inside the dropPinFor method, so your tuple is redeclared you need only assign the value in the dropPinFor
Declaration
var coordinate : (Double, Double) = (0,0)
Code
func dropPinFor(placemark: MKPlacemark) {
selectedItemPlacemark = placemark
for annotation in mapView.annotations {
if annotation.isKind(of: MKPointAnnotation.self) {
// mapView.removeAnnotation(annotation) // removing the pins from the map
}
}
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.coordinate
mapView.addAnnotation(annotation)
self.coordinate = (placemark.coordinate.latitude, placemark.coordinate.longitude)
print("This is the pins destinations coord \(destLat, destLong)")
}

Is there a way to pre-populate the GMSAutocompleteViewController with a data array of current nearby locations?

I've tried to generate a likelihood list of places with the currentPlaceWithCallback, which works fine. However, I'm having difficult figuring out a way to insert the information gathered from this likelihood list into the view of the GMSAutocompleteViewController / GMSAutocompleteResultsViewController before the user starts typing a search (this functionality works fine).
Any suggestions would be greatly appreciated!
func generateLikelihoodListViaPlacesClient() {
placesClient.currentPlaceWithCallback { (likelihoodlist, error) -> Void in
if error != nil {
println("Current Place error: \(error!.localizedDescription)")
return
}
for likelihood in likelihoodlist!.likelihoods {
let nearestPlace = likelihoodlist!.likelihoods.first
println(nearestPlace)
if let likelihood = likelihood as? GMSPlaceLikelihood {
let place = likelihood.place
self.placesArray!.insert(place.name, atIndex: 0)
println("Current Place name \(place.name) at likelihood \(likelihood.likelihood)")
println("Current Place address \(place.formattedAddress)")
println("Current Place attributions \(place.attributions)")
println("Current PlaceID \(place.placeID)")
}
}
}
}
My current GMSAutocompleteViewController is presented after clicking on a UITextField and triggering the EditingDidBegin textfield delegate function. I've set the bounds by using my location manager which has already been previously used:
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let newLocation = locations.last {
println("#######################")
println("\(newLocation.coordinate)")
println("#######################")
// Creates northeast and southwest coordinate bounds for rectangle that autocompleteController returns results in
let currentCoordinate2D : CLLocationCoordinate2D = newLocation.coordinate
let northEastCoordinate2DWithBearing : CLLocationCoordinate2D = locationWithBearing(270, distanceMeters: 500, origin: currentCoordinate2D)
let southWestCoordinate2DWithBearing : CLLocationCoordinate2D = locationWithBearing(90, distanceMeters: 500, origin: currentCoordinate2D)
let bounds : GMSCoordinateBounds = GMSCoordinateBounds(coordinate: northEastCoordinate2DWithBearing, coordinate: southWestCoordinate2DWithBearing)
autocompleteController.autocompleteBounds = bounds
// Restricts filter to only businesses
let filter : GMSAutocompleteFilter = GMSAutocompleteFilter()
filter.type = .Establishment
autocompleteController.autocompleteFilter = filter
if #available(iOS 9.0, *) {
} else {
// Fallback on earlier versions
locationManager.stopUpdatingLocation()
}
}
}

Resources