Mapbox Navigation in iOS with in my mapView controller - ios

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

Related

Using Google Places API to find nearby places BY TYPE (parks) for iOS SwiftUI

I'm pretty new to Swift/iOS app dev. so far, so I'm struggling to figure this out. Basically, I'm trying to make it so when the app opens (first screen is the map), it automatically finds nearby places that are only parks around the user's current location and have these locations annotated with markers on a map (Google Maps) using Google Places API for iOS using updated SwiftUI/Swift 5.0: Using no storyboards!. Table I types, in this case, parks: https://developers.google.com/places/web-service/supported_types
So far, this is the code I have... It uses GMSPlaceLikelihood of places nearby the user's location. This is more so an example of what I want to achieve, however using Google Places API nearby search instead so I can show only parks. Image of app running:
Image
(The place's found from nearby are then listed in a table as shown on the image. This list is just for show)
Thanks in advance for any advice/help.
GoogleMapsView.swift:
#ObservedObject var locationManager = LocationManager()
#ObservedObject var place = PlacesManager()
func makeUIView(context: Self.Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(withLatitude: locationManager.latitude, longitude: locationManager.longitude, zoom: 14)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.isMyLocationEnabled = true
mapView.settings.rotateGestures = false
mapView.settings.tiltGestures = false
mapView.isIndoorEnabled = false
mapView.isTrafficEnabled = false
mapView.isBuildingsEnabled = false
mapView.settings.myLocationButton = true
place.currentPlacesList(completion: { placeLikelihoodList in
if let placeLikelihoodList = placeLikelihoodList {
print("total places: \(placeLikelihoodList.count)")
for likelihood in placeLikelihoodList {
let place = likelihood.place
let position = CLLocationCoordinate2D(latitude: place.coordinate.latitude, longitude: place.coordinate.longitude)
let marker = GMSMarker(position: position)
marker.title = place.name
marker.map = mapView
}
}
})
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
// let camera = GMSCameraPosition.camera(withLatitude: locationManager.latitude, longitude: locationManager.longitude, zoom: zoom)
// mapView.camera = camera
mapView.animate(toLocation: CLLocationCoordinate2D(latitude: locationManager.latitude, longitude: locationManager.longitude))
}
PlacesManager.swift:
class PlacesManager: NSObject, ObservableObject {
private var placesClient = GMSPlacesClient.shared()
#Published var places = [GMSPlaceLikelihood]()
override init() {
super.init()
currentPlacesList { (places) in
guard let places = places else {
return
}
self.places = places
}
}
func currentPlacesList(completion: #escaping (([GMSPlaceLikelihood]?) -> Void)) {
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue) | UInt(GMSPlaceField.types.rawValue) | UInt(GMSPlaceField.coordinate.rawValue))!
placesClient.findPlaceLikelihoodsFromCurrentLocation(withPlaceFields: fields, callback: {
(placeLikelihoodList: Array<GMSPlaceLikelihood>?, error: Error?) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let placeLikelihoodList = placeLikelihoodList {
for likelihood in placeLikelihoodList {
let place = likelihood.place
}
completion(placeLikelihoodList)
}
})
}
ContentView.swift:
var body: some View {
VStack {
GoogleMapsView()
.edgesIgnoringSafeArea(.top)
.frame(height: 400)
PlacesList()
}
.offset(y: 100)
}
https://developers.google.com/places/web-service/search
I suggest you visit this page, it describes the API in detail and shows examples how to call it with different parameters.
After you get the response (I suggest getting it in json format and using SwiftyJSON cocoa pod to parse it) populate the table.

Mapkit, how to change annotation coordinates to nearest address?

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

How to get single dataBase reference from Firebase

I'm sharing and retrieving coordinates with Firebase, but when I print them in my console..I get same coordinates 3-4 time.
Which creates an odd effect on my custom marker image file.
How can I get the coordinates from Firebase only once?
Here is my code:
var posts=[postStruct]()
var mapView : GMSMapView? = nil
var friendLocator : [Locator] = [Locator]()
struct Locator {
let name: String
let long: CLLocationDegrees
let lat: CLLocationDegrees
}
var latPass: Double!
var longPass: Double!
var fetchLat: Double!
var fetchLong: Double!
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
var location=locations[0]
let span:MKCoordinateSpan=MKCoordinateSpanMake(0.01, 0.01)
var myLocation:CLLocationCoordinate2D=CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region:MKCoordinateRegion=MKCoordinateRegionMake(myLocation, span)
latPass=28.3217378
longPass=75.6895935
post()
self.configureMapView()
let dataBaseRef=FIRDatabase.database().reference()
dataBaseRef.child("Raunak Trikha").queryOrderedByKey().observeSingleEvent(of: .childAdded, with: {(snapshot) in
let postDict = snapshot.value as? [String : AnyObject] ?? [:]
var fetchLat = postDict["lat"] as! Double
var fetchLong = postDict["long"] as! Double
let locator = Locator(name: "Raunak Trikha", long: fetchLong, lat: fetchLat)
self.friendLocator.append(locator)
self.locateFriend()
print(fetchLat)
print(fetchLong)
})
manager.stopUpdatingLocation()
self.view = mapView
}
func locateFriend() {
for friend in friendLocator{
let friendMarker = GMSMarker()
friendMarker.position=CLLocationCoordinate2D(latitude: friend.lat, longitude: friend.long)
friendMarker.title=friend.name
friendMarker.map=mapView
mapView?.selectedMarker=friendMarker
if friend.name=="Virat Singh"{
friendMarker.icon=UIImage(named: "ViratPin.png")
}
else if friend.name=="Raunak Trikha"{
friendMarker.icon=UIImage(named: "currentLocation.png")
}
}
do {
mapView?.mapStyle = try GMSMapStyle(jsonString: kMapStyle)
} catch {
NSLog("One or more of the map styles failed to load. \(error)")
}
}
func configureMapView(){
let camera = GMSCameraPosition.camera(withLatitude: latPass, longitude: longPass, zoom: 10)
self.mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
view = mapView
mapView?.settings.scrollGestures = true
mapView?.settings.zoomGestures = true
mapView?.settings.myLocationButton = true
//mapView?.addSubview(searchBar)
//mapView?.addSubview(searchSupporter)
//mapView?.bringSubview(toFront: searchBar)
for gesture in (mapView?.gestureRecognizers!)! {
mapView?.removeGestureRecognizer(gesture)
}
}
when I print fetchLat & fetchLong I get the same coordinates 4 time, which overlaps my custom marker image that creates the weird effect.
Since your code that adds a particular Locator struct is called multiple times, check your array to make sure it doesn't already contain the exact same struct before adding it to the array locally.
This will evaluate your array of structs and determine if there is no value for it. But it also assumes that name property of the struct is a unique identifier for each struct, which may not be your case. You can alternatively compare any value within the filter closure that you want to make sure isn't duplictated, i. e. lat and long.
let locator = Locator(name: "Raunak Trikha", long: fetchLong, lat: fetchLat)
if self.friendLocator.filter({ $0.name == locator.name }).count == 0 {
self.friendLocator.append(locator)
}
self.locateFriend()
This function func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) will get called whenever your location changes/updates or until the GPS settles (Warms up) on your location.
I notice you are using the firebase single event obeserver function for database updates using .observeSingleEvent() which is correct however since you have defined the call in the above didUpdateLocations function it will be called multiple times.
Either move the call to Firebase out of the function or supply some conditional to call firebase only once. I.e only update if the location has changed more than X range/distance etc.

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

Getting coordinates of user's location (OneShotLocationManager)

I am currently trying to make a weather app. It is using the http://developer.forecast.io API, which accepts longitude and latitude coordinates to find the weather. I have OneShotLocationManager implemented, but I can't quite get the coordinates from it then use them with the Forecast API.
Here's my code:
var manager: OneShotLocationManager?
let coordinate: (lat: Double, long: Double) = (0,0)
override func viewDidLoad() {
super.viewDidLoad()
manager = OneShotLocationManager()
manager!.fetchWithCompletion { location, error in
// fetch location or an error
if let loc = location {
print(location?.coordinate.latitude)
print(location?.coordinate.longitude)
let coordinate: (lat: Double, long: Double) = ((location?.coordinate.latitude)!,location!.coordinate.longitude)
} else if let err = error {
print(err.localizedDescription)
}
self.manager = nil
} }
I can get OneShotLocationManager to print the coordinates to the console, but I can't figure out how to actually use them with Forecast.io. Any help on this would be greatly appreciated.
You have no need to use location var anymore, for that reason you do the if let check.
Try this:
if let loc = location {
let coordinate = (lat: loc.coordinate.latitude, long: loc.coordinate.longitude)
// ...your web API request stuff using coordinate....
}

Resources