Calculating distance between 3 or more addresses using MapKit - ios

Using the following code I can calculate the distance between two addresses. Is it possible to add the ability to add more addresses to the mix to calculate the distance between 3 or more?
Code so far:
import Cocoa
import MapKit
import CoreLocation
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.
}
})
}
})
}
}

There are multiple ways to achieve this. Since you can make multiple requests at the same time and you need to wait for them I suggest you to create all of them simultaneously. When all return you should then return your result. There are many tools to achieve this but the most direct way is to simply create a counter. You start it with number of expected requests and then decrease it for every returned requests. Once the number turns to zero all are done. A simple example would be:
var count: Int = 10
for _ in 0..<count {
DispatchQueue.main.asyncAfter(deadline: .now() + .random(in: 1..<10)) {
count -= 1
if count == 0 {
print("All done here")
}
}
}
For your case you may want to restructure a bit. I think you should first geocode addresses to get all the placemarks. Then use array of placemarks to create pairs to use directions. Doing so will not duplicate for geocoding: Assume you are having 3 addresses A, B, C. Then to do pairs directly your would do distance(geocode(A), geocode(B)) + distance(geocode(B), geocode(C)) and you are geocoding B twice. So to avoid it rather do distance([A, B, C].map { geocode($0) }).
From your example this is what I got:
private static func generatePlacemarksFromAddresses(_ addresses: [String], completion: #escaping ((_ placemarks: [(address: String, placemark: MKPlacemark)]?, _ error: Error?) -> Void)) {
guard addresses.count > 0 else {
completion([], nil)
return
}
var requestsOut: Int = addresses.count
var placemarks: [String: MKPlacemark?] = [String: MKPlacemark?]()
addresses.forEach { address in
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { foundPlacemarks, error in
let placemark: MKPlacemark? = {
guard let location = foundPlacemarks?.first?.location else { return nil }
return MKPlacemark(coordinate: location.coordinate)
}()
placemarks[address] = placemark
requestsOut -= 1
if requestsOut == 0 {
// All are finished
// Compose ordered array or error
let erroredAddresses: [String] = placemarks.filter { $0.value == nil }.map { $0.key }
if erroredAddresses.count > 0 {
completion(nil, NSError(domain: "GEOCODING", code: 400, userInfo: ["dev_message": "Not all adresses could be geocoded. Failed with \(erroredAddresses.count) addressess: \(erroredAddresses.joined(separator: " & "))"]))
} else {
completion(addresses.map { ($0, placemarks[$0]!!) }, nil)
}
}
}
}
}
private static func calculateDirectionDistanceFromPlacemarks(_ placemarks: [(address: String, placemark: MKPlacemark)], completion: #escaping ((_ distance: Double?, _ error: Error?) -> Void)) {
guard placemarks.count > 1 else {
completion(0, nil)
return
}
var requestsOut: Int = placemarks.count-1
var overallDistance: Double = 0.0
var erroredConnections: [String] = [String]()
for index in 0..<placemarks.count-1 {
let directions = MKDirections(request: {
let request: MKDirections.Request = MKDirections.Request()
request.source = MKMapItem(placemark: placemarks[index].placemark)
request.destination = MKMapItem(placemark: placemarks[index+1].placemark)
request.transportType = MKDirectionsTransportType.automobile
return request
}())
directions.calculate(completionHandler: { (response: MKDirections.Response?, error: Error?) in
if let distance = response?.routes.first?.distance {
overallDistance += distance
} else {
erroredConnections.append(placemarks[index].address + " -> " + placemarks[index+1].address)
}
requestsOut -= 1
if requestsOut == 0 {
// All are done
if erroredConnections.count > 0 {
completion(nil, NSError(domain: "GEOCODING", code: 400, userInfo: ["dev_message": "Not all connections returned a route. Failed with \(erroredConnections.count) connections: \(erroredConnections.joined(separator: " & "))"]))
} else {
completion(overallDistance, nil)
}
}
})
}
}
And usage is pretty simple:
generatePlacemarksFromAddresses(["Ljubljana", "Canada", "1 Pall Mall East, London SW1Y 5AU", "Buckingham Palace, London SW1A 1AA", "1 Pall Mall East, London SW1Y 5AU"]) { placemarks, error in
guard let placemarks = placemarks else {
print("Placemarks could not be generated. Got error: \(error)")
return
}
calculateDirectionDistanceFromPlacemarks(placemarks) { distance, error in
if let distance = distance {
print("Got distance: \(distance)")
}
if let error = error {
print("Got error: \(error)")
}
}
}
Note that a given example produces an error because there is no root across Atlantic which is a valid result. To test with non-error example you can remove "Canada" and try with ["Ljubljana", "1 Pall Mall East, London SW1Y 5AU", "Buckingham Palace, London SW1A 1AA", "1 Pall Mall East, London SW1Y 5AU"].

Related

Loop through coordinates and find the closest shop to a point Swift 3

Idea :
App lets drivers see the closest shop/restaurants to customers.
What I have :
Coordinates saved as strings
let clientLat = "24.449384"
let clientLng = "56.343243"
a function to find all the shops in my local area
I tried to save all the coordinates of a shop in my local area and I succeeded:
var coordinates: [CLLocationCoordinate2D] = [CLLocationCoordinate2D]()
func performSearch() {
coordinates.removeAll()
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = "starbucks"
request.region = mapView.region
let search = MKLocalSearch(request: request)
search.start(completionHandler: {(response, error) in
if error != nil {
print("Error occured in search: \(error!.localizedDescription)")
} else if response!.mapItems.count == 0 {
print("No matches found")
} else {
print("Matches found")
for item in response!.mapItems {
self.coordinates.append(item.placemark.coordinate)
// need to sort coordinates
// need to find the closest
let annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name
self.mapView.addAnnotation(annotation)
}
}
})
}
What I need:
I wish to loop through the coordinates and find the closest shop (kilometers) to the lat and long strings then put a pin on it.
UPDATE
func performSearch() {
coordinates.removeAll()
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = "starbucks"
request.region = mapView.region
let search = MKLocalSearch(request: request)
search.start(completionHandler: {(response, error) in
if error != nil {
print("Error occured in search: \(error!.localizedDescription)")
} else if response!.mapItems.count == 0 {
print("No matches found")
} else {
print("Matches found")
for item in response!.mapItems {
self.coordinates.append(item.placemark.coordinate)
let pointToCompare = CLLocation(latitude: 24.741721, longitude: 46.891440)
let storedCorrdinates = self.coordinates.map({CLLocation(latitude: $0.latitude, longitude: $0.longitude)}).sorted(by: {
$0.distance(from: pointToCompare) < $1.distance(from: pointToCompare)
})
self.coordinate = storedCorrdinates
}
let annotation = MKPointAnnotation()
annotation.coordinate = self.coordinate[0].coordinate
self.mapView.addAnnotation(annotation)
}
})
}
Thank you #brimstone
You can compare distances between coordinates by converting them to CLLocation types and then using the distance(from:) method. For example, take your coordinates array and map it to CLLocation, then sort that based on the distance from the point you are comparing them to.
let coordinates = [CLLocationCoordinate2D]()
let pointToCompare = CLLocation(latitude: <#yourLat#>, longitude: <#yourLong#>)
let sortedCoordinates = coordinates.map({CLLocation(latitude: $0.latitude, longitude: $0.longitude)}).sorted(by: {
$0.distance(from: pointToCompare) < $1.distance(from: pointToCompare)
})
Then, to set your annotation's coordinate to the nearest coordinate, just subscript the sortedCoordinates array.
annotation.coordinate = sortedCoordinates[0].coordinate
I would like to share my solution :)
1) In my case, I upload data from the API, so I need to create a model.
import MapKit
struct StoresMap: Codable {
let id: Int?
let title: String?
let latitude: Double?
let longitude: Double?
let schedule: String?
let phone: String?
let ukmStoreId: Int?
var distanceToUser: CLLocationDistance?
}
The last variable is not from API, but from myself to define distance for each store.
2) In ViewController I define:
func fetchStoresList() {
NetworkManager.downloadStoresListForMap(firstPartURL: backendURL) { (storesList) in
self.shopList = storesList
let initialLocation = self.locationManager.location!
for i in 0..<self.shopList.count {
self.shopList[i].distanceToUser = initialLocation.distance(from: CLLocation(latitude: self.shopList[i].latitude!, longitude: self.shopList[i].longitude!))
}
self.shopList.sort(by: { $0.distanceToUser! < $1.distanceToUser!})
print("Closest shop - ", self.shopList[0])
}
}
3) Don't forget to call the function in viewDidLoad() and import MapView framework :)

BAD_EXC_ACCESS for attempting to load view controller while deallocating

I'm getting a BAD_EXC_ACCESS on line . The reason is "Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior".
func drawLocations(loc: CLLocation)
{
let center = CLLocationCoordinate2D(latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude)
let lat: CLLocationDegrees = center.latitude
let long: CLLocationDegrees = center.longitude
var points = [CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long)]
let polygon = MKPolygon(coordinates: &points, count: points.count)
mapView.addOverlay(polygon)//where I get error
}
func loadLocation(completion: (error:NSError?, records:[CKRecord]?) -> Void)
{
let query = CKQuery(recordType: "Location", predicate: NSPredicate(value: true))
CKContainer.defaultContainer().publicCloudDatabase.performQuery(query, inZoneWithID: nil){
(records, error) in
if error != nil {
print("error fetching locations: \(error)")
completion(error: error, records: nil)
} else {
print("found locations: \(records)")
print("found locations")
completion(error: nil, records: records)
guard let records = records else {
return
}
for(var i = 0; i<records.count; i += 1)
{
self.drawLocations(records[i]["location"] as! CLLocation)//where I call function
}
}
}
}
The completion block of performQuery "must be capable of running on any thread of the app" (as described in the docs). You call addOverlay which is a UI function, and so much be called on the main queue. You need to dispatch this method to the main queue.
Side note, unrelated to the question: for(var i = 0; i<records.count; i += 1) is much better written as for record in records. The C-style syntax is deprecated.

iOS - How can I return latitude and longitude?

I can't return latitude and longitude. I always get a 0.0 and 0.0.
What can I do to get these values?
Code :
func forwardGeocoding (address: String) -> (Double, Double) {
let geoCoder = CLGeocoder()
var latitude: Double = 0.0
var longitude: Double = 0.0
geoCoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]?, error: NSError?) -> Void in
if error != nil {
print(error?.localizedDescription)
} else {
if placemarks!.count > 0 {
let placemark = placemarks![0] as CLPlacemark
let location = placemark.location
latitude = Double((location?.coordinate.latitude)!)
longitude = Double((location?.coordinate.longitude)!)
print("before : \(latitude, longitude)")
}
}
}
print("after : \(latitude, longitude)")
return (latitude, longitude)
}
This my viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
forwardGeocoding("New York, NY, United States")
}
Result:
after : (0.0, 0.0)
before : (40.713054, -74.007228)
The problem with your code is that geocoding requests are asynchronous, so the return statement is executed before the geocoding results are actually retrieved.
I'd probably use one of two options to fix this. First, instead of returning a tuple, make your own completion handler, and call it after the placemark is found:
func forwardGeocoding (address: String, completion: (CLLocationCoordinate2D) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]?, error: NSError?) -> Void in
if error != nil {
print(error?.localizedDescription)
} else {
if placemarks!.count > 0 {
let placemark = placemarks![0] as CLPlacemark
let location = placemark.location
completion(location.coordinate)
}
}
}
}
Now when you call this function you can provide the completion with the relevant values from wherever you're calling the function.
If the function is actually a method in a class and it never needs to be called from another class, you could have it set properties of the class, and those properties could have didSet blocks. For example:
class SomeClass {
var coordinates: CLLocationCoordinate2D {
didSet {
doSomethingWithCoordinates()
}
}
private func forwardGeocoding (address: String, completion: (CLLocationCoordinate2D) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]?, error: NSError?) -> Void in
if error != nil {
print(error?.localizedDescription)
} else {
if placemarks!.count > 0 {
let placemark = placemarks![0] as CLPlacemark
let location = placemark.location
self.coordinates = location.coordinate
}
}
}
}
}
The first options is probably more versatile, but the second avoids having completion blocks withing completion blocks, which can sometimes become confusing to keep track of in your code.
It's quite obvious that your function returns always (0.0, 0.0). It's because geoCoder.geocodeAddressString() returns placemarks asynchronously.
It's time-consuming operation so you have to modify your code to handle that.
One of possible solutions is to modify your function:
func forwardGeocoding (address: String, completion: (Bool, CLLocationCoordinate2D!) -> () ) {
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]?, error: NSError?) -> Void in
if error != nil {
print(error?.localizedDescription)
completion(false,nil)
} else {
if placemarks!.count > 0 {
let placemark = placemarks![0] as CLPlacemark
let location = placemark.location
completion(true, location?.coordinate)
}
}
}
And call it:
self.forwardGeocoding(YourAdress, completion {
success, coordinate in
if success {
let lat = coordinate.lattitude
let long = coordinate.longitude
// Do sth with your coordinates
} else {
// error sth went wrong
}
}
Notice that your function returns nothing.
Hope that helps.

Is it possible to have custom reverse geocoding?

Currently, I'm utilizing reverse geocoding to simply convert a longitude and latitude to a locality and sub locality.
Is it possible for me to override this and essentially have it return custom strings at my discretion given that I provide it with the coordinates? Any thoughts?
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
locationManager.stopUpdatingLocation()
if(locations.count > 0){
let location = locations[0] as! CLLocation
// println(location.coordinate)
if let currentLocatino = currLocation {
if CLLocation(latitude: currentLocatino.latitude, longitude: currentLocatino.longitude).distanceFromLocation(location) > 500 {
currLocation = location.coordinate
self.skip = 0
self.loadObjects()
}
}
else {
currLocation = location.coordinate
self.skip = 0
self.loadObjects()
}
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: currLocation!.latitude, longitude: currLocation!.longitude), completionHandler: {(placemarks, error) -> Void in
if error != nil {
println("Reverse geocoder failed with error" + error.localizedDescription)
return
}
if placemarks.count > 0 {
let date = NSDate()
let formatter = NSDateFormatter()
formatter.dateStyle = .MediumStyle
formatter.stringFromDate(date)
let pm = placemarks[0] as! CLPlacemark
var testifempty = "\(pm.subLocality)"
if testifempty == "nil"
{
self.locationManager.startUpdatingLocation()
if let lbutton = self.lbutton{
lbutton.text = "Hello " + "\(pm.locality)" //+ "\n" + formatter.stringFromDate(date)
}
}
else
{
self.locationManager.startUpdatingLocation()
if let lbutton = self.lbutton {
lbutton.text = "Hello " + "\(pm.subLocality)\n" // + formatter.stringFromDate(date)
}
}
}
else {
println("Problem with the data received from geocoder")
}
})
} else {
println("Cannot fetch your location")
}
}
If you know what the names are, you might try a switch that swapped out matching strings.
var locale_string
switch locale_string {
case “name1”:
let displayed_locale_name = “changed_name1”
case “name2”:
let displayed_locale_name = “changed_name2”
...
}
Then, the default case where you haven't identified the locale could accept the string as it is.
I got the idea from the Swift2 Programming Language Guide, page 17.
Reverse geocoding is basically a spatial query. I would suggest to define a map with spatial boundaries, assign each boundary an attribute for both locality and sub-locality.
With that map, you can run your set of coordinates against it and assign them the same attributes as the coordinate fits in one of the spatial boundaries.

Save to parse manually after data has been queried

I am using Parse pinInBackground feature to pin data (image, text, date, coordinates) in the background and that data is queried every time the app is opened.The app is used to log a photo and the location and coordinates.So every entry you make is queried and displays in a tableview (only the count of entries yet).
I want to be able to let the user manually sink with Parse.com and not use the saveEventually feature.
Meaning I want a button and when pressed the queried data must sink with Parse and the be in pinned.
Here is how my data is pinned
#IBAction func submitButton(sender: AnyObject) {
locationLogs["title"] = log.title
locationLogs["description"] = log.descriptionOf
println("log = \(log.title)")
println("log = \(log.descriptionOf)")
locationLogs.pinInBackgroundWithBlock { (success: Bool, error: NSError?) -> Void in
if (success) {
}else{
println("error = \(error)")
}}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
let location:CLLocationCoordinate2D = manager.location.coordinate
let altitude: CLLocationDistance = manager.location.altitude
println("new long= \(location.longitude)")
println("new lat= \(location.latitude)")
println("new altitude= \(altitude)")
println("new timestamp = \(timestamp)")
locationLogs["longitude"] = location.longitude
locationLogs["latitude"] = location.latitude
locationLogs["altitude"] = altitude
locationLogs["timestamp"] = timestamp
}
And here I query it
var result : AnyObject?
override func viewDidLoad() {
super.viewDidLoad()
let query = PFQuery(className:"LocationLogs")
query.fromLocalDatastore()
query.findObjectsInBackgroundWithBlock( { (NSArray results, NSError error) in
if error == nil && results != nil {
self.result = results
println("count = \(self.result!.count)")
self.loggedItemsTableView.reloadData()
}else{
println("ERRRROOOORRRR HORROOORRR= \(error)")
}
})
}
I have tried to use this command:
result?.saveAllInBackground()
But this only gave back an error.
Can someone please give me the correct code on how to do this or give me a link showing me how.
Here is a full code explanation on how I solved it:
//create an Array
var tableData: NSArray = []
func queryAll() {
let query = PFQuery(className:"LocationLogs")
query.fromLocalDatastore()
query.findObjectsInBackgroundWithBlock( { (NSArray results, NSError error) in
if error == nil && results != nil {
println("array = \(results)" )
self.tableData = results!
self.loggedItemsTableView.reloadData()
}else{
println("ERRRROOOORRRR HORROOORRR= \(error)")
}
})
}
//Call save on the class and not the object
PFObject.saveAllInBackground(self.tableData as [AnyObject], block: { (success: Bool, error: NSError?) -> Void in
if (success) {
//Remember to unpin the data if it will no longer be needed
PFObject.unpinAllInBackground(self.tableData as [AnyObject])
println("Pinned Data has successfully been saved")
}else{
println("error= \(error?.localizedDescription)")
}
})
Use this to safely cast/checked your object
if let results = self.result { // this will verify if your self.result is a non-nil array of object
// if it has a value then it will be passed to results
// you can now safely proceed on saving your objects
PFObject.saveAllInBackground(results, block: { (succeeded, error) -> Void in
// additional code
if succeeded {
// alert, remove hud ......
}else{
if let reqError = error {
println(reqError.localizedDescription)
}
}
})
}
struct log {
// this is dummy datastructure that imitate some part of your code ... you dont need it
var title = ""
var descriptionOf = ""
}
struct LocationInfo{
var title:String!
var description:String!
var location:PFGeoPoint!
var timestamp:NSDate!
var username:String
}
var locationManager = CLLocationManager()
var arrayOfLocations = [LocationInfo]()
func submitButton(sender: AnyObject) {
var logData = log()
var point = OneLocation(locationManager)
let username = PFUser.currentUser()?.username
var locationLogs = PFObject(className: "")
locationLogs["title"] = logData.title
locationLogs["description"] = logData.descriptionOf
locationLogs["Location"] = point
locationLogs["username"] = username
locationLogs["TimeStamp"] = locationManager.location.timestamp
locationLogs.saveInBackgroundWithBlock { (success:Bool, error:NSError?) -> Void in
if error == nil
{
println("data was save")
}
}
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
CLGeocoder().reverseGeocodeLocation (manager.location, completionHandler: {(placemarks, error)->Void in
var UserCurrentLocation:CLLocationCoordinate2D = manager.location.coordinate
// println("User's parking Location : \(UserCurrentLocation.latitude) \(UserCurrentLocation.longitude)")
if (error != nil) {
println("Error")
return
}
locationManager.stopUpdatingLocation()
})
}
func OneLocation(Manager:CLLocationManager)->PFGeoPoint{
var latitude = Manager.location.coordinate.latitude
var longitude = Manager.location.coordinate.longitude
var point = PFGeoPoint(latitude: latitude, longitude: longitude)
return point
}
func QueryFromParse(){
var query = PFQuery(className: "")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let new_objects = objects as? [PFObject]
{
for SingleObject in new_objects
{
// with this single object you could get the description, title , username,etc
var location = SingleObject["Location"] as! PFGeoPoint
var username = SingleObject["username"] as! String
var title = SingleObject["title"] as! String
var time = SingleObject["TimeStamp"] as! NSDate
var description = SingleObject["description"] as! String
var singleLocationInfo = LocationInfo(title: title, description: description, location: location, timestamp: time, username: username)
arrayOfLocations.append(singleLocationInfo)
// reload data for the tableview
}
}
}
else
{
println("error")
}
}
}
Therefore you have an arrayoflocations that can be used to populate data in your tableView
Struct log is a dummy datastructure
And also I don't know if that was what you wanted ... but you should get the idea..
Hope that helps..

Resources