I'm a newbie in swift and I'm trying to calculate the distance, on route, from userLocation to several Point of interest.
I don’t want, in this part of the app, “draw” the route on a map, but I only want to calculate the distance on route instead the distance between two coordinate and then show this distance as information inside the callout on the map.
The coordinate of the POI (latitude and longitude) are contained in a database.
I have read some threads about this argument here on Stack Overflow:
Measuring distance in meters from a drawn route in MKMapView
MKMapView get distance between coordinates on customized route
and other tutorials:
http://www.technetexperts.com/mobile/draw-route-between-2-points-on-map-with-ios7-mapkit-api/
https://videos.raywenderlich.com/courses/mapkit-and-core-location/lessons/9
then i wrote this code:
for item in items {
if item.latitudine != "" && item.longitudine != "" {
// Point Of Interest coordinate
let latitude = Double(item.latitude!)
let longitude = Double(item.longitude!)
let itemLocation = CLLocation(latitude: latitude!, longitude: longitude!)
let itemLocationPlacemark = MKPlacemark(coordinate: itemLocation.coordinate, addressDictionary: nil)
// user coordinate
let userLocation = CLLocation(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
let userLocationPlacemark = MKPlacemark(coordinate: userLocation.coordinate, addressDictionary: nil)
// create Request object
let request = MKDirectionsRequest()
request.source = MKMapItem(placemark: userLocationPlacemark)
request.destination = MKMapItem(placemark: itemLocationPlacemark)
request.requestsAlternateRoutes = false
request.transportType = .automobile
let directions = MKDirections(request: request)
directions.calculate {
[weak self] (response, error) in
if error == nil {
for route in (response?.routes)! {
let distance = (route.distance)/1000
print(distance)
}
}
}
}
}
The problem is when I execute the code from the line directions.calculate.
The program run the line but then don’t execute the rest, don’t execute the control if error == nil and the instructions in the closure.
So now I wonder if my idea is wrong and, if not, how can obtain my goal.
(Posted solution on behalf of the OP).
Reading other threads I understand that the problem was the closure inside che for loop. So after several attempts I found the solution that work for me. I write it here so that can be useful to someone else:
var counter: Int!
...
for item in itemPin {
if item.latitude != "" && item.longitude != "" {
....
....
let directions = MKDirections(request: request)
directions.calculate {
(response, error) in
print("error \(error)")
if error == nil {
counter = self.counter + 1
print(self.counter)
if self.counter >= self.itemPin.count {
let result = (response?.routes[0].distance)!/1000
}
}
}
...
}
}
Related
I used this function for an geo query. But I don't known how to add the document from the query to an array. So I can display some Map Annotations with infos from an Firestore document. How should I change it?
func geoQuery() {
// [START fs_geo_query_hashes]
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50
// 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("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 <= 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)
}
// [END fs_geo_query_hashes]
}
https://firebase.google.com/docs/firestore/solutions/geoqueries?hl=en#swift_2 This is the Firebase documentary.
I don't know how your documents are structured or how your map is configured to display data (annotations versus regions, for example), but the general fix for your problem is to coordinate the loop of queries in your function and give them a completion handler. And to do that, we can use a Dispatch Group. In the completion handler of this group, you have an array of document snapshots which you need to loop through to get the data (from each document), construct the Pin, and add it to the map. There are a number of other steps involved here that I can't help you with since I don't know how your documents and map are configured but this will help you. That said, you could reduce this code a bit and make it more efficient but let's just go with the Firebase sample code you're using and get it working first.
struct Pin: Identifiable {
let id = UUID().uuidString
var location: MKCoordinateRegion
var name: String
var img: String
}
func geoQuery() {
// [START fs_geo_query_hashes]
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50
// 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("cities")
.order(by: "geohash")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}
// Create a dispatch group outside of the query loop since each iteration of the loop
// performs an asynchronous task.
let dispatch = DispatchGroup()
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))")
dispatch.leave() // leave the dispatch group when we exit this completion
return
}
for document in documents {
let lat = document.data()["lat"] as? Double ?? 0
let lng = document.data()["lng"] as? Double ?? 0
let name = document.data()["names"] as? String ?? "no name"
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 <= radiusInKilometers {
matchingDocs.append(document)
}
}
dispatch.leave() // leave the dispatch group when we exit this completion
}
// 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 {
dispatch.enter() // enter the dispatch group on each iteration
query.getDocuments(completion: getDocumentsCompletion)
}
// [END fs_geo_query_hashes]
// This is the completion handler of the dispatch group. When all of the leave()
// calls equal the number of enter() calls, this notify function is called.
dispatch.notify(queue: .main) {
for doc in matchingDocs {
let lat = doc.data()["lat"] as? Double ?? 0
let lng = doc.data()["lng"] as? Double ?? 0
let name = doc.data()["names"] as? String ?? "no name"
let coordinates = CLLocation(latitude: lat, longitude: lng)
let region = MKCoordinateRegion(center: <#T##CLLocationCoordinate2D#>, latitudinalMeters: <#T##CLLocationDistance#>, longitudinalMeters: <#T##CLLocationDistance#>)
let pin = Pin(location: region, name: name, img: "someImg")
// Add pin to array and then to map or just add pin directly to map here.
}
}
}
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, annotationItems: pvm.allPins) { pin in
MapAnnotation(coordinate: pin.location.coordinate) {
Image(pin.img)
}
}
I calculate the route and I want to add a pin annotation at the end...
declare location, send request, declare directions and reset my map view with new directions...
guard let location = locationManager.location?.coordinate else { return }
let request = createDirectionsRequest(from: location)
let directions = MKDirections(request: request)
resetMapView(withNew: directions)
after I calculate the direction and add annotation...
directions.calculate { [unowned self] (response, error) in
guard let response = response else { return }
for route in response.routes {
self.mapView.add(route.polyline, level: MKOverlayLevel.aboveRoads)
self.mapView.setCenter(route.polyline.coordinate, animated: true)
self.mapView.setVisibleMapRect(route.polyline.boundingMapRect, animated: true)
self.mapView.setRegion(MKCoordinateRegionMakeWithDistance(route.polyline.coordinate, route.distance*0.75, route.distance*0.75), animated: true)
let routeAnnotation = MKPointAnnotation()
routeAnnotation.coordinate = MKCoordinateForMapPoint(route.polyline.points()[route.polyline.pointCount/2])
self.mapView.addAnnotation(routeAnnotation)
}
}
and this is the result... the annotation is not in the right position of my route... where i'm wrong?
UPDATE
In my case I solved the problem...
I select my destination with a pointer positioned on center of the view... move the map and return me the center coordinates with a function that i write for calculate it:
func getCenterLocation(for mapView: MKMapView) -> CLLocation {
let latidude = mapView.centerCoordinate.latitude
let longitude = mapView.centerCoordinate.longitude
return CLLocation(latitude: latidude, longitude: longitude)
}
I simply call this func for determine my pin coordinates that are the same of the center of the view (pointer) and consequently are the same the destination point...
routeAnnotation.coordinate = self.getCenterLocation(for: self.mapView).coordinate
and this is the result
more solutions are accepted...
I would like to get the distance between two locations.
I tried this code:
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
CLGeocoder().geocodeAddressString("ADDRESS OF LOC 2") { (placemarks, error) in
guard let placemarks = placemarks, let loc2 = placemarks.first?.location
else {
return
}
let loc1 = CLLocation(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
var distance = loc1.distance(from: loc2)
print((distance/1000).rounded(), " km")
}
}
My problem is that i get a wrong distance.
The print result will be "2 km"
If i calculate the distance between the same locations in "Maps" I get two route options with the length "2,7 km" and "2,9 km"
What did i wrong?
From the documentation:
This method measures the distance between the two locations by tracing a line between them that follows the curvature of the Earth. The resulting arc is a smooth curve and does not take into account specific altitude changes between the two locations.
So you're calculating straight line distance, "as the crow flies", not following roads, which would lead to a longer route, as you're seeing in Maps. You'd need something like MapKit's MKDirectionsRequest to match the route you see in the Maps app. Ray Wenderlich has a good tutorial.
Here's an example I just knocked up that works in a macOS Playground:
//: Playground - noun: a place where people can play
import Cocoa
import MapKit
import CoreLocation
// As we're waiting for completion handlers, don't want the playground
// to die on us just because we reach the end of the playground.
// See https://stackoverflow.com/questions/40269573/xcode-error-domain-dvtplaygroundcommunicationerrordomain-code-1
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let geocoder = CLGeocoder()
geocoder.geocodeAddressString("1 Pall Mall East, London SW1Y 5AU") { (placemarks: [CLPlacemark]? , error: Error?) in
if let placemarks = placemarks {
let start_placemark = placemarks[0]
geocoder.geocodeAddressString("Buckingham Palace, London SW1A 1AA", completionHandler: { ( placemarks: [CLPlacemark]?, error: Error?) in
if let placemarks = placemarks {
let end_placemark = placemarks[0]
// Okay, we've geocoded two addresses as start_placemark and end_placemark.
let start = MKMapItem(placemark: MKPlacemark(coordinate: start_placemark.location!.coordinate))
let end = MKMapItem(placemark: MKPlacemark(coordinate: end_placemark.location!.coordinate))
// Now we've got start and end MKMapItems for MapKit, based on the placemarks. Build a request for
// a route by car.
let request: MKDirectionsRequest = MKDirectionsRequest()
request.source = start
request.destination = end
request.transportType = MKDirectionsTransportType.automobile
// Execute the request on an MKDirections object
let directions = MKDirections(request: request)
directions.calculate(completionHandler: { (response: MKDirectionsResponse?, error: Error?) in
// Now we should have a route.
if let routes = response?.routes {
let route = routes[0]
print(route.distance) // 2,307 metres.
}
})
}
})
}
}
If you want travel distance rather than "as the crow flies", use MKDirections, e.g.:
func routes(to item: MKMapItem, completion: #escaping ([MKRoute]?, Error?) -> Void) {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = item
request.transportType = .automobile
let directions = MKDirections(request: request)
directions.calculate { response, error in
completion(response?.routes, error)
}
}
And if you want to format to one decimal place, I'd suggest using NumberFormatter, so it's appropriately formatted for international users for whom the decimal separator is not .:
self.routes(to: item) { routes, error in
guard let routes = routes, error == nil else {
print(error?.localizedDescription ?? "Unknown error")
return
}
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 1
formatter.maximumFractionDigits = 1
for route in routes {
let distance = route.distance / 1000
print(formatter.string(from: NSNumber(value: distance))!, "km")
}
}
Using MapKit & Swift 5
Calculate distance between two location location, It will help anyone
Sample Function : I have tested in Google Map as well as Apple Map
let startLocation : CLLocation = CLLocation.init(latitude: 23.0952779, longitude: 72.5274129)
let endLocation : CLLocation = CLLocation.init(latitude: 23.0981711, longitude: 72.5294229)
let distance = startLocation.distance(from: endLocation)
self.getDistance(departureDate: Date().adjust(hour: 8, minute: 0, second: 0, day: 0, month: 0), arrivalDate: Date().adjust(hour: 8, minute: 10, second: 0, day: 0, month: 0), startLocation: startLocation, endLocation: endLocation) { (distanceInMeters) in
print("fake distance: \(distance)")
let fakedistanceInMeter = Measurement(value: distance, unit: UnitLength.meters)
let fakedistanceInKM = fakedistanceInMeter.converted(to: UnitLength.kilometers).value
let fakedistanceInMiles = fakedistanceInMeter.converted(to: UnitLength.miles).value
print("fakedistanceInKM :\(fakedistanceInKM)")
print("fakedistanceInMiles :\(fakedistanceInMiles)")
print("actualDistance : \(distanceInMeters)")
let distanceInMeter = Measurement(value: distanceInMeters, unit: UnitLength.meters)
let distanceInKM = distanceInMeter.converted(to: UnitLength.kilometers).value
let distanceInMiles = distanceInMeter.converted(to: UnitLength.miles).value
print("distanceInKM :\(distanceInKM)")
print("distanceInMiles :\(distanceInMiles)")
}
Use of functions
self.getDistance(departureDate: trip.departure.dateTime, arrivalDate: trip.arrival.dateTime, startLocation: startLocation, endLocation: endLocation) { (actualDistance) in
print("actualDistance : \(actualDistance)")
}
I am improved above function and added code here, I hope it will help someone.
func calculateDistancefrom(departureDate: Date, arrivalDate: Date, sourceLocation: MKMapItem, destinationLocation: MKMapItem, doneSearching: #escaping (_ distance: CLLocationDistance) -> Void) {
let request: MKDirections.Request = MKDirections.Request()
request.departureDate = departureDate
request.arrivalDate = arrivalDate
request.source = sourceLocation
request.destination = destinationLocation
request.requestsAlternateRoutes = true
request.transportType = .automobile
let directions = MKDirections(request: request)
directions.calculate { (directions, error) in
if var routeResponse = directions?.routes {
routeResponse.sort(by: {$0.expectedTravelTime <
$1.expectedTravelTime})
let quickestRouteForSegment: MKRoute = routeResponse[0]
doneSearching(quickestRouteForSegment.distance)
}
}
}
func getDistance(departureDate: Date, arrivalDate: Date, startLocation : CLLocation, endLocation : CLLocation, completionHandler: #escaping (_ distance: CLLocationDistance) -> Void) {
let destinationItem = MKMapItem(placemark: MKPlacemark(coordinate: startLocation.coordinate))
let sourceItem = MKMapItem(placemark: MKPlacemark(coordinate: endLocation.coordinate))
self.calculateDistancefrom(departureDate: departureDate, arrivalDate: arrivalDate, sourceLocation: sourceItem, destinationLocation: destinationItem, doneSearching: { distance in
completionHandler(distance)
})
}
I am trying to get the latitude en longitude coordinates of the CLLocationManager instance of the current user's location.
I have those two lines of code in my viewWillAppear method:
locationManager = CLLocationManager()
locationManager.requestWhenInUseAuthorization()
Then I have a method called startRoute to calc the route from the current position to some specific annotation on the map. After that I draw the route between those paths. Unfortunately, this is working with two annotations on the map, but I can't get it work with the current location of the user and some annotation.
I tried below script, but when I print print("LAT \(currentLocation.coordinate.latitude)") it will give me 0.0 as result.
func startRoute() {
// Set the latitude and longtitude of the locations (markers)
let sourceLocation = CLLocationCoordinate2D(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
let destinationLocation = CLLocationCoordinate2D(latitude: sightseeing.latitude!, longitude: sightseeing.longitude!)
// Create placemark objects containing the location's coordinates
let sourcePlacemark = MKPlacemark(coordinate: sourceLocation, addressDictionary: nil)
let destinationPlacemark = MKPlacemark(coordinate: destinationLocation, addressDictionary: nil)
// MKMapitems are used for routing. This class encapsulates information about a specific point on the map
let sourceMapItem = MKMapItem(placemark: sourcePlacemark)
let destinationMapItem = MKMapItem(placemark: destinationPlacemark)
// Annotations are added which shows the name of the placemarks
let sourceAnnotation = MKPointAnnotation()
sourceAnnotation.title = "Times Square"
if let location = sourcePlacemark.location {
sourceAnnotation.coordinate = location.coordinate
}
let destinationAnnotation = MKPointAnnotation()
destinationAnnotation.title = "Title"
destinationAnnotation.subtitle = "Subtitle"
if let location = destinationPlacemark.location {
destinationAnnotation.coordinate = location.coordinate
}
// The annotations are displayed on the map
self.mapView.showAnnotations([sourceAnnotation,destinationAnnotation], animated: true )
// The MKDirectionsRequest class is used to compute the route
let directionRequest = MKDirectionsRequest()
directionRequest.source = sourceMapItem
directionRequest.destination = destinationMapItem
directionRequest.transportType = .walking
// Calculate the direction
let directions = MKDirections(request: directionRequest)
// The route will be drawn using a polyline as a overlay view on top of the map.
// The region is set so both locations will be visible
directions.calculate {
(response, error) -> Void in
guard let response = response else {
if let error = error {
print("Error: \(error)")
}
return
}
let route = response.routes[0]
self.mapView.add((route.polyline), level: MKOverlayLevel.aboveRoads)
let rect = route.polyline.boundingMapRect
self.mapView.setRegion(MKCoordinateRegionForMapRect(rect), animated: true)
}
}
At the top, directly after the class declaration I create the reference to location manager and 'CLLocation':
var locationManager: CLLocationManager!
var currentLocation = CLLocation()
After all, this isn't working. Only when I change the sourceLocation to hardcoded coordinates, it will draw the route to the destination. What do I wrong or what am I missing?
but when I print print("LAT \(currentLocation.coordinate.latitude)") it will give me 0.0 as result
Of course it does. What else would it do? You have a property
var currentLocation = CLLocation()
That is a zero location. You have no code that ever changes this. Therefore, it is always a zero location.
You say you want the
current user's location
but nowhere do you have any code that obtains the user's location.
I want to create the application that can calculate the driving distance between point A to point B. I know that the CLLocation has distanceFromLocation: to calculate the distance between 2 points but it calculate only the straight line from point A to B. Does it has the way to calculate the driving distance between 2 points? How? Any sample?
Thanks
Swift 3:
func routingDistance(userNotation: CLLocation, destinationLocation: CLLocation, completion: #escaping (CLLocationDistance) -> Void) {
let request:MKDirectionsRequest = MKDirectionsRequest()
// source and destination are the relevant MKMapItems
let sourceS = CLLocationCoordinate2D(latitude: userNotation.coordinate.latitude, longitude: userNotation.coordinate.longitude)
let destinationD = CLLocationCoordinate2D(latitude: destinationLocation.coordinate.latitude, longitude: destinationLocation.coordinate.longitude)
let sourcePM = MKPlacemark(coordinate: sourceS)
let destinationPM = MKPlacemark(coordinate: destinationD)
request.source = MKMapItem(placemark: sourcePM)
request.destination = MKMapItem(placemark: destinationPM)
// 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.calculate { (response, error) in
if let response = response, let route = response.routes.first {
completion(route.distance)
}
}
}
I don't think there is any functionality in iOS, distanceFromLocation will give the air distance between two co-ordinates.
but yes you can calculate road distance using google APIS.
Swift 5+:
If you are looking for driving distance, you can always use MKDirections.
Here is the code for finding driving distance (You can also find walking, and transit distance by changing transport type).
let sourceP = CLLocationCoordinate2DMake( sourceLat, sourceLong)
let destP = CLLocationCoordinate2DMake( desLat, desLong)
let source = MKPlacemark(coordinate: sourceP)
let destination = MKPlacemark(coordinate: destP)
let request = MKDirections.Request()
request.source = MKMapItem(placemark: source)
request.destination = MKMapItem(placemark: destination)
// Specify the transportation type
request.transportType = MKDirectionsTransportType.automobile;
// If you want only the shortest route, set this to a false
request.requestsAlternateRoutes = true
let directions = MKDirections(request: request)
// Now we have the routes, we can calculate the distance using
directions.calculate { (response, error) in
if let response = response, let route = response.routes.first {
print(route.distance) //This will return distance in meters
}
}
If you are only looking for air distance/bird's eye distance/coordinate distance, you can use this code:
let sourceP = CLLocation(latitude: sourceLat, longitude: sourceLong)
let desP = CLLocation(latitude: desLat, longitude: desLong))
let distanceInMeter = sourceP.distance(from: desP)