Display route on map in Swift - ios

I am trying to draw the route between two points on Apple map (Swift code).
The following structure is used to store the coordinates
struct GeoLocation {
var latitude: Double
var longitude: Double
func distanceBetween(other: GeoLocation) -> Double {
let locationA = CLLocation(latitude: self.latitude, longitude: self.longitude)
let locationB = CLLocation(latitude: other.latitude, longitude: other.longitude)
return locationA.distanceFromLocation(locationB)
}
}
self.foundLocations - is an array of these structures
In the custom class I recieve the coordinates of the points on the map.
var coordinates = self.foundLocations.map{$0.coordinate}
Then I draw the route on the map
self.polyline = MKPolyline(coordinates: &coordinates, count: coordinates.count)
self.mapView.addOverlay(self.polyline, level: MKOverlayLevel.AboveRoads)
To draw the route I use the following method from MKMapViewDelegate
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
if let polylineOverlay = overlay as? MKPolyline {
let render = MKPolylineRenderer(polyline: polylineOverlay)
render.strokeColor = UIColor.blueColor()
return render
}
return nil
}
Instead of the actual route laying on roads I get just a straight line between two points.
How can I display the actual route?

You actually have to fetch the route from Apple's maps' server using calculateDirectionsWithCompletionHandler.
First create the relevant MKMapItems for both the source and destination, ex:
let geocoder = CLGeocoder()
let location = CLLocation(latitude: sourceLatitude, longitude: sourceLongitude)
geocoder.reverseGeocodeLocation(location, completionHandler: {
(placemarks:[AnyObject]?, error:NSError?) -> Void in
if placemarks?.count > 0 {
if let placemark: MKPlacemark = placemarks![0] as? MKPlacemark {
self.source = MKMapItem(placemark: placemark)
}
}
})
(Repeat for destination.)
Then fetch the MKRoute, ex:
let request:MKDirectionsRequest = MKDirectionsRequest()
// source and destination are the relevant MKMapItems
request.setSource(source)
request.setDestination(destination)
// Specify the transportation type
request.transportType = MKDirectionsTransportType.Automobile;
// If you're open to getting more than one route,
// requestsAlternateRoutes = true; else requestsAlternateRoutes = false;
request.requestsAlternateRoutes = true
let directions = MKDirections(request: request)
directions.calculateDirectionsWithCompletionHandler ({
(response: MKDirectionsResponse?, error: NSError?) in
if error == nil {
self.directionsResponse = response
// Get whichever currentRoute you'd like, ex. 0
self.route = directionsResponse.routes[currentRoute] as MKRoute
}
})
Then after retrieving the MKRoute, you can add the polyline to the map like so:
mapView.addOverlay(route.polyline, level: MKOverlayLevel.AboveRoads)

Swift 3 and reusable conversion of Lyndsey Scott's answer:
final class Route {
static func getRouteFor(
source: CLLocationCoordinate2D,
destination: CLLocationCoordinate2D,
completion: #escaping (
_ route: MKRoute?,
_ error: String?)->()
) {
let sourceLocation = CLLocation(
latitude: source.latitude,
longitude: source.longitude
)
let destinationLocation = CLLocation(
latitude: destination.latitude,
longitude: destination.longitude
)
let request = MKDirectionsRequest()
self.getMapItemFor(location: sourceLocation) { sourceItem, error in
if let e = error {
completion(nil, e)
}
if let s = sourceItem {
self.getMapItemFor(location: destinationLocation) { destinationItem, error in
if let e = error {
completion(nil, e)
}
if let d = destinationItem {
request.source = s
request.destination = d
request.transportType = .walking
let directions = MKDirections(request: request)
directions.calculate(completionHandler: { response, error in
if let r = response {
let route = r.routes[0]
completion(route, nil)
}
})
}
}
}
}
}
static func getMapItemFor(
location: CLLocation,
completion: #escaping (
_ placemark: MKMapItem?,
_ error: String?)->()
) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemark, error in
if let e = error {
completion(nil, e.localizedDescription)
}
if let p = placemark {
if p.count < 1 {
completion(nil, "placemark count = 0")
} else {
if let mark = p[0] as? MKPlacemark {
completion(MKMapItem(placemark: mark), nil)
}
}
}
}
}
}
Usage:
Route.getRouteFor(source: CLLocationCoordinate2D, destination: CLLocationCoordinate2D) { (MKRoute?, String?) in
<#code#>
}

Related

Use variables inside a completion handler somewhere else

I'm trying to convert address to coordinates in order to create a Firestorm GeoPoint object.
I currently have this code:
func getCoords(from address: String, locationCompletionHandler: #escaping (CLLocationCoordinate2D?, Error?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemarks, error) in
guard
let placemarks = placemarks,
let coordinate = placemarks.first?.location?.coordinate
else {
locationCompletionHandler(nil, error)
return
}
locationCompletionHandler(coordinate, nil)
}
}
func addressToGeoPoint(from address: String) -> GeoPoint {
var latitude:Double = 0
var longitude:Double = 0
getCoords(from: address) { coordinate, error in
if let coordinate = coordinate {
latitude = coordinate.latitude
longitude = coordinate.longitude
}
else {
print("Can't get coords: \(String(describing: error?.localizedDescription))")
}
}
return GeoPoint(latitude: latitude, longitude: longitude)
}
The problem is that when the GeoPoint object is being initialized, the latitude and longitude variables are still 0 because the completion handler hasn't finished yet.
The function addressToGeoPoint must return a GeoPoint.
What can I do in order for this to work?
Thanks!

Iterate through JSON array and add coordinates to the map

I'm using an API to get latitude and longitude coordinates and place them on a map with the name of the place it corresponds to. I'm able to put one place's lat and long coordinates but I'm not too sure how to add all of them to a map. I can't get my head around how to do it. I've tried to use a for loop to do it but I'm too sure on how I would implement it. This is what I've got so far:
func getData() {
let url = "https://www.givefood.org.uk/api/2/foodbanks/"
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { [self] data, response, error in
guard let data = data, error == nil else {
print("Wrong")
return
}
var result: [Info]?
do {
result = try JSONDecoder().decode([Info].self, from: data)
}
catch {
print("Failed to convert: \(error.localizedDescription)")
}
guard let json = result else {
return
}
for each in json {
var each = 0
each += 1
let comp = json[each].lat_lng?.components(separatedBy: ",")
let latString = comp![each]
let lonString = comp![each]
let lat = Double(latString)
let lon = Double(lonString)
let locationPin: CLLocationCoordinate2D = CLLocationCoordinate2DMake(lat!, lon!)
let location: CLLocationCoordinate2D = CLLocationCoordinate2DMake(51.55573, -0.108312)
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMetres, longitudinalMeters: regionInMetres)
mapView.setRegion(region, animated: true)
let myAn1 = MapPin(title: json[each].name!, locationName: json[each].name!, coordinate: locationPin)
mapView.addAnnotations([myAn1])
}
})
task.resume()
}
Your loop is wrong, each after for is one Info item. The Int index each is pointless and you set it in each iteration to zero so you get always the same coordinate (at index 1).
First of all declare name and lat_lng as non-optional. All records contain both fields.
struct Info : Decodable {
let lat_lng : String
let name : String
}
Second of all for convenience reasons extend CLLocationCoordinate2D to create a coordinate from a string
extension CLLocationCoordinate2D {
init?(string: String) {
let comp = string.components(separatedBy: ",")
guard comp.count == 2, let lat = Double(comp[0]), let lon = Double(comp[1]) else { return nil }
self.init(latitude: lat, longitude: lon )
}
}
Third of all put all good code into the do scope instead of dealing with optionals and set the region once before the loop
func getData() {
let url = "https://www.givefood.org.uk/api/2/foodbanks/"
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { [self] data, response, error in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode([Info].self, from: data!)
let location = CLLocationCoordinate2D(latitude: 51.55573, longitude: -0.108312)
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMetres, longitudinalMeters: regionInMetres)
mapView.setRegion(region, animated: true)
var pins = [MapPin]()
for info in result {
if let coordinate = CLLocationCoordinate2D(string: info.lat_lng) {
pins.append(MapPin(title: info.name, locationName: info.name, coordinate: coordinate))
}
}
DispatchQueue.main.async {
self.mapView.addAnnotations(pins)
}
}
catch {
print("Failed to convert: \(error)")
}
})
task.resume()
}

How to draw a polyline between multiple markers?

Want to draw a PolyLine from userLocation to multiple marker. In my code already added markers coordinates in a array then added userLocation into 0th position of that array. Now I want to draw a route polyLine between array elements. My code is given below...
self.coods.append(self.currentLocation)
let jsonResponse = response.data
do{
let json = try JSON(data: jsonResponse!)
self.dictXYZ = [json]
print("JsonResponse printed \(json["data"][0]["lattitude"])")
if let array = json["data"].array{
for i in 0..<array.count{
var coordinate = CLLocationCoordinate2D()
coordinate.latitude = array[i]["lattitude"].doubleValue
coordinate.longitude = array[i]["longitude"].doubleValue
self.coods.append(coordinate)
}
for j in self.coods {
let marker = GMSMarker()
marker.position = j
let camera = GMSCameraPosition.camera(withLatitude: j.latitude, longitude: j.longitude, zoom: 12)
self.mapView.camera = camera
marker.map = self.mapView
}
let path = GMSMutablePath()
for j in self.coods {
path.add(j)
}
let polyline = GMSPolyline(path: path)
polyline.map = mapView
In the Google Developer Docs.
Waypoints - Specifies an array of intermediate locations to include along the route between the origin and destination points as
pass through or stopover locations. Waypoints alter a route by
directing it through the specified location(s). The API supports
waypoints for these travel modes: driving, walking and bicycling; not
transit.
First you need to create a waypoints for all intermediate locations to add the route between the source and destination. With that polyline you can create a GMSPath and then draw the route by using GMSPolyline. I hope below solution can help you to draw a route for multiple locations.
func getPolylineRoute(from source: CLLocationCoordinate2D, to destinations: [CLLocationCoordinate2D], completionHandler: #escaping (Bool, String) -> ()) {
guard let destination = destinations.last else {
return
}
var wayPoints = ""
for (index, point) in destinations.enumerated() {
if index == 0 { // Skipping first location that is current location.
continue.
}
wayPoints = wayPoints.count == 0 ? "\(point.latitude),\(point.longitude)" : "\(wayPoints)%7C\(point.latitude),\(point.longitude)"
}
let url = URL(string: "https://maps.googleapis.com/maps/api/directions/json?origin=\(source.latitude),\(source.longitude)&destination=\(destination.latitude),\(destination.longitude)&sensor=true&mode=driving&waypoints=\(wayPoints)&key=\(GOOGLE_API_KEY)")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Failed : \(String(describing: error?.localizedDescription))")
return
} else {
do {
if let json: [String: Any] = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any] {
guard let routes = json["routes"] as? [[String: Any]] else { return }
if (routes.count > 0) {
let overview_polyline = routes[0]
let dictPolyline = overview_polyline["overview_polyline"] as? NSDictionary
let points = dictPolyline?.object(forKey: "points") as? String
completionHandler(true, points!)
} else {
completionHandler(false, "")
}
}
} catch {
print("Error : \(error)")
}
}
}
task.resume()
}
Pass the current location and destination array of locations to getPolylineRoute method. Then call the drawPolyline method with polyline points from main thread.
getPolylineRoute(from: coods[0], to: coods) { (isSuccess, polylinePoints) in
if isSuccess {
DispatchQueue.main.async {
self.drawPolyline(withMapView: self.mapView, withPolylinePoints: polylinePoints)
}
} else {
print("Falied to draw polyline")
}
}
func drawPolyline(withMapView googleMapView: GMSMapView, withPolylinePoints polylinePoints: String){
path = GMSPath(fromEncodedPath: polylinePoints)!
let polyline = GMSPolyline(path: path)
polyline.strokeWidth = 3.0
polyline.strokeColor = .lightGray
polyline.map = googleMapView
}
First create GMSPath object
let path = GMSMutablePath()
self.coods.forEach {
path.add(coordinate: $0)
}
https://developers.google.com/maps/documentation/ios-sdk/reference/interface_g_m_s_mutable_path.html#af62038ea1a9da3faa7807b8d22e72ffb
Then Create GMSPolyline object using path
let pathLine = GMSPolyline.with(path: path)
pathLine.map = self.mapView
https://developers.google.com/maps/documentation/ios-sdk/reference/interface_g_m_s_polyline.html#ace1dd6e6bab9295b3423712d2eed90a4

Update polyline as user moves

I am using GoogleMaps to draw route. What I want to do is when user travels on that route remove the line which is already travelled(Like Uber does). I guess we can do it with removing the points from the polyline and redraw it. Is it the correct approach?
How can I know that those points are travelled and need to update the path?
1) Create Globle Variable
var demoPolyline = GMSPolyline()
var demoPolylineOLD = GMSPolyline()
// Set Destination Location Cordinates
var destinationLocation = CLLocation(latitude: 23.072837, longitude: 72.516455)
2) Use CLLocationManagerDelegate Method For update current location
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location: CLLocation = locations.last!
let originalLoc: String = "\(location.coordinate.latitude),\(location.coordinate.longitude)"
let destiantionLoc: String = "\(destinationLocation.coordinate.latitude),\(destinationLocation.coordinate.longitude)"
let latitudeDiff: Double = Double(location.coordinate.latitude) - Double(destinationLocation.coordinate.latitude)
let longitudeDiff: Double = Double(location.coordinate.longitude) - Double(destinationLocation.coordinate.longitude)
let waypointLatitude = location.coordinate.latitude - latitudeDiff
let waypointLongitude = location.coordinate.longitude - longitudeDiff
getDirectionsChangedPolyLine(origin: originalLoc, destination: destiantionLoc, waypoints: ["\(waypointLatitude),\(waypointLongitude)"], travelMode: nil, completionHandler: nil)
}
3) Create Method For Draw and update Polyline on Google map
func getDirectionsChangedPolyLine(origin: String!, destination: String!, waypoints: Array<String>!, travelMode: AnyObject!, completionHandler: ((_ status: String, _ success: Bool) -> Void)?)
{
DispatchQueue.main.asyncAfter(deadline: .now()) {
if let originLocation = origin {
if let destinationLocation = destination {
var directionsURLString = "https://maps.googleapis.com/maps/api/directions/json?" + "origin=" + originLocation + "&destination=" + destinationLocation
if let routeWaypoints = waypoints {
directionsURLString += "&waypoints=optimize:true"
for waypoint in routeWaypoints {
directionsURLString += "|" + waypoint
}
}
directionsURLString = directionsURLString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!
let directionsURL = NSURL(string: directionsURLString)
DispatchQueue.main.async( execute: { () -> Void in
let directionsData = NSData(contentsOf: directionsURL! as URL)
do{
let dictionary: Dictionary<String, AnyObject> = try JSONSerialization.jsonObject(with: directionsData! as Data, options: JSONSerialization.ReadingOptions.mutableContainers) as! Dictionary<String, AnyObject>
let status = dictionary["status"] as! String
if status == "OK" {
self.selectedRoute = (dictionary["routes"] as! Array<Dictionary<String, AnyObject>>)[0]
self.overviewPolyline = self.selectedRoute["overview_polyline"] as! Dictionary<String, AnyObject>
let route = self.overviewPolyline["points"] as! String
let path: GMSPath = GMSPath(fromEncodedPath: route)!
self.demoPolylineOLD = self.demoPolyline
self.demoPolylineOLD.strokeColor = UIColor.blue
self.demoPolylineOLD.strokeWidth = 3.0
self.demoPolylineOLD.map = self.mapView
self.demoPolyline.map = nil
self.demoPolyline = GMSPolyline(path: path)
self.demoPolyline.map = self.mapView
self.demoPolyline.strokeColor = UIColor.blue
self.demoPolyline.strokeWidth = 3.0
self.demoPolylineOLD.map = nil
} else {
self.getDirectionsChangedPolyLine(origin: origin, destination: destination, waypoints: waypoints, travelMode: travelMode, completionHandler: completionHandler)
}
} catch {
self.getDirectionsChangedPolyLine(origin: origin, destination: destination, waypoints: waypoints, travelMode: travelMode, completionHandler: completionHandler)
}
})
} else {
print("Destination Location Not Found")
}
} else {
print("Origin Location Not Found")
}
}
}
This is working on my live project
I hope this will work for everybody

I'm trying to draw polyline on coordinates(lat, long) coming from API using swift 3 in map kit but unable to make polyline

import UIKit
import MapKit
import CoreLocation
import AddressBook
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var TheMap: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
zoomToRegion()
location()
}
func centerMapOnLocation(location: MKPointAnnotation, regionRadius: Double) {
let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate,
regionRadius * 2.0, regionRadius * 2.0)
TheMap.setRegion(coordinateRegion, animated: true)
}
//MARK:- MapViewDelegate methods
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let polylineRenderer = MKPolylineRenderer(overlay: overlay)
if overlay is MKPolyline {
polylineRenderer.strokeColor = UIColor.blue
polylineRenderer.lineWidth = 5
}
return polylineRenderer
}
//MARK:- Zoom to region
func zoomToRegion() {
let location = CLLocationCoordinate2D(latitude: 28.618945, longitude: 77.377347400000005)
let region = MKCoordinateRegionMakeWithDistance(location, 5000.0, 7000.0)
TheMap.setRegion(region, animated: true)
}
// API CALL FUNCTION
func location() {
let user = "userid"
let password = "password"
let postString = ["empid":user, "date1": password]
var request = URLRequest(url:URL(string: "http://mydomainhere.com/airtel_hrm/webapi/api/getpunchdeytails")!)
request.httpMethod = "POST"
request.httpBody = try! JSONSerialization.data(withJSONObject: postString, options:.prettyPrinted)
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?)in
if error != nil
{
print("error=\(error)")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any],
let data = json["punchdetails"] as? [[String: Any]] {
//print(data)
for datas in data {
let lat = datas["punch_loc_lat"] as! String
let long = datas["punch_loc_long"] as! String
var annotations = [MKPointAnnotation]()
let latitude = CLLocationDegrees(lat)
let longitude = CLLocationDegrees(long)
let coordinate = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotations.append(annotation)
self.TheMap.addAnnotations(annotations)
self.TheMap.delegate = self
self.centerMapOnLocation(location: annotations[0], regionRadius: 2000.0)
// Connect all the mappoints using Poly line.
var points: [CLLocationCoordinate2D] = [coordinate] //[CLLocationCoordinate2D]()
for annotation in annotations {
points.append(annotation.coordinate)
}
print("this is points = \(points)")
let polyline = MKPolyline(coordinates: &points, count: points.count)
//self.TheMap.add(polyline)
} //for loop closed
}
} catch {
print(error)
}
}
task.resume()
}
}
I'm sure if you shared the results of the print statement, the problem would have been obvious. You're probably seeing lots of print statements. Bottom line, you should define your array outside of the for loop, only append values within the loop, and then add the polyline after the loop:
do {
if let json = try JSONSerialization.jsonObject(with: data!) as? [String: Any],
let data = json["punchdetails"] as? [[String: Any]] {
var annotations = [MKPointAnnotation]()
for datas in data {
let lat = datas["punch_loc_lat"] as! String
let long = datas["punch_loc_long"] as! String
let latitude = CLLocationDegrees(lat)
let longitude = CLLocationDegrees(long)
let coordinate = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotations.append(annotation)
}
self.TheMap.delegate = self // you really should do this in IB or, if you feel compelled to do it programmatically, in viewDidLoad
// Connect all the mappoints using Poly line.
let points = annotations.map { $0.coordinate }
print("this is points = \(points)")
let polyline = MKPolyline(coordinates: &points, count: points.count)
DispatchQueue.main.async {
self.centerMapOnLocation(location: annotations[0], regionRadius: 2000.0)
self.TheMap.addAnnotations(annotations)
self.TheMap.add(polyline)
}
}
} catch {
print(error)
}
Note, I'd also do all interaction with the map view from the main queue.

Resources