Sort Array by Calculated Distance in Swift

tableview sort by distance swift

import UIKit
import CoreLocation

final class Places {
var title: String?
var cllocation: CLLocation
var regionRadius: Double
var location: String?
var type: String?
var distance : Double?
var coordinate : CLLocationCoordinate2D

init(title:String , cllocation: CLLocation , regionRadius: Double, location: String, type: String ,distance:Double!,coordinate: CLLocationCoordinate2D){
self.title = title
self.cllocation = cllocation
self.coordinate = coordinate
self.regionRadius = regionRadius
self.location = location
self.type = type
self.distance = distance
}


// Function to calculate the distance from given location.
func calculateDistance(fromLocation: CLLocation?) {

distance = cllocation.distanceFromLocation(fromLocation!)
}
}


let fromLocation:CLLocation = CLLocation(latitude: 24.186965, longitude: 120.633268)

var places:[Places] = [


Places( title: "Title1", cllocation: CLLocation( latitude :24.181143, longitude: 120.593158), regionRadius: 300.0, location: "LocationTitle1", type: "Food",distance : CLLocation( latitude :24.181143, longitude: 120.593158).distanceFromLocation(fromLocation),coordinate : CLLocationCoordinate2DMake(24.181143,120.593158)),
Places( title: "Title2", cllocation: CLLocation(latitude:24.14289,longitude:120.679901), regionRadius:150.0, location:"LocationTitle2",type: "Food",distance : CLLocation(latitude:24.14289,longitude:120.679901).distanceFromLocation(fromLocation),coordinate : CLLocationCoordinate2DMake(24.14289,120.679901)),
Places( title: "Title3", cllocation: CLLocation(latitude : 24.180407, longitude:120.645086), regionRadius: 300.0, location:"LocationTitle3", type: "Food",distance : CLLocation(latitude : 24.180407, longitude:120.645086).distanceFromLocation(fromLocation),coordinate : CLLocationCoordinate2DMake(24.180407,120.645086)),
Places( title: "Title4", cllocation: CLLocation(latitude: 24.149062,longitude:120.684891), regionRadius: 300.0, location: "LocationTitle4", type: "Food",distance : CLLocation(latitude: 24.149062,longitude:120.684891).distanceFromLocation(fromLocation),coordinate : CLLocationCoordinate2DMake(24.149062,120.684891)),
Places( title: "Title5", cllocation: CLLocation(latitude:24.138598,longitude:120.672096 ), regionRadius:150.0, location:"LocationTitle5",type: "Food",distance : CLLocation(latitude:24.138598,longitude:120.672096 ).distanceFromLocation(fromLocation),coordinate : CLLocationCoordinate2DMake(24.138598,120.672096)),
Places( title: "Title6", cllocation: CLLocation(latitude :24.1333434,longitude:120.680744), regionRadius:100.0, location:"LocationtTitle6",type: "Culture",distance : CLLocation(latitude :24.1333434,longitude:120.680744).distanceFromLocation(fromLocation),coordinate : CLLocationCoordinate2DMake(24.1333434,120.680744))

]

for k in 0...(places.count-1) {
print("\(places[k].distance)")
}
for place in places {
place.calculateDistance(fromLocation) // Replace YOUR_LOCATION with the location you want to calculate the distance to.
}


places.sortInPlace({ $0.distance < $1.distance })

for n in 0...(places.count-1) {
print("\(places[n].distance)")
}

Output :

//before sort array
Optional(4126.1395817058)
Optional(6803.61030342841)
Optional(1403.39181021788)
Optional(6718.92222011204)
Optional(6653.47447563344)
Optional(7651.92757760459)


//after sort array
Optional(1403.39181021788)
Optional(4126.1395817058)
Optional(6653.47447563344)
Optional(6718.92222011204)
Optional(6803.61030342841)
Optional(7651.92757760459)

How to sort array with respect to distance calculated from latitude and longitude.

Almost the same as previous answer but without adding field:

CLLocation *yourLocation = [[CLLocation alloc] initWithLatitude:.... longitude:...];
NSArray *array = @[@{ @"name": @"NYC", @"lat": @1.11, @"lng": @2.22}, @{@"name":@"CAL", @"lat":@3.33, @"lng":@4.44}, @{@"name":@"LA", @"lat":@5.55, @"lng":@6.66}];
array = [array sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
CLLocation *location1 = [[CLLocation alloc] initWithLatitude:[obj1[@"lat"] double] longitude:[obj1[@"lng"] double]];
CLLocation *location2 = [[CLLocation alloc] initWithLatitude:[obj2[@"lat"] double] longitude:[obj2[@"lng"] double]];

CLLocationDistance distance1 = [location1 distanceFromLocation:yourLocation];
CLLocationDistance distance2 = [location2 distanceFromLocation:yourLocation];

if (distance1 > distance2) {
return (NSComparisonResult)NSOrderedDescending;
}

if (distance1 < obj2 distance2) {
return (NSComparisonResult)NSOrderedAscending;
}
return (NSComparisonResult)NSOrderedSame;
}];

Sort array by distance from current location

Here's a solution. It's a little more complicated that a straightforward call to a sort function for a couple of reasons: firstly the need to find the closest location for the organization, as mentioned in the question, and secondly the fact that the haversine calculation used inside CLLocation's distanceFromLocation: method can slow things down if used naively.

For those reasons I've created a special object to do the sort, so that I can use a member dictionary to memoize the results of calls to distanceFromLocation. This won't make a difference for the test data, but will matter if you ever need to deal with a large set of places.

As a side note - it might make things a bit simpler if OrganizationObject stored a CLLocation and not a CLLocationCoordinate - though this is a fairly minor issue.

Here's the code:

class OrganizationSorter {

var memoizedValues = [Int:CLLocationDistance]()

private func shortestDistanceToOrganizationFromLocation(organization:OrganizationObject,location:CLLocation) -> CLLocationDistance? {

let memoizedValue = memoizedValues[organization.id] //Check whether we've done this calculation before, if so return the result from last time
if memoizedValue != nil {
return memoizedValue
}

//There should probably be some code around here to check
//that the organization object has at least one location
//I'm assuming it does to simplify things

var shortestDistance : CLLocationDistance? = nil
let locations = organization.locations
if locations.count > 0 {
for coord in locations {
let loc = CLLocation(latitude: coord.latitude, longitude: coord.longitude)
let dist = loc.distanceFromLocation(location)

if shortestDistance == nil || shortestDistance > dist {
shortestDistance = dist
}
}
}

if shortestDistance != nil {
memoizedValues[organization.id] = shortestDistance
}

return shortestDistance
}

func sortOrganizationsByDistanceFromLocation(orgArray:[OrganizationObject],location:CLLocation) -> [OrganizationObject] {
let sortedArray = orgArray.sort { (a:OrganizationObject, b:OrganizationObject) -> Bool in
let dist1 = self.shortestDistanceToOrganizationFromLocation(a, location: location)
let dist2 = self.shortestDistanceToOrganizationFromLocation(b, location: location)
return dist1 < dist2
}
memoizedValues.removeAll() //reset memoized values in case object is used twice
return sortedArray
}
}

I've tested it on your example data, using the location of the Christiansborg Palace in Copenhagen as a test loc, and got the following ordering:

Kongens have
Statens Museum For Kunst
Magasin du nord
7 eleven

which seems to match up to the coordinates given - it looks like the nearest coords for Magasin du Nord are somewhere in a town on the outskirts of Copenhagen (the other one's in Lille?), and 7 eleven is in Sweden.

Here is how the class is used (using the test data from the original question, with the id values in the OrganizationObjects changed so they are not all 0 (otherwise the code will not work).

let location = CLLocation(latitude: 55.676251, longitude: 12.580570) //Christiansborg Palace, chosen since it is relatively near the other locations, to make it obvious whether results are sensible or not

let orgSorter = OrganizationSorter()

let sortedLocations = orgSorter.sortOrganizationsByDistanceFromLocation(orgArray, location: location)

for org in orgArray {
print(org.name)
}

print("\n")

for org in sortedLocations {
print(org.name)
}

How can I sort an array of Business objects by distance from user?

Given this struct (you can also use a class)

struct Business {
let latitude: Double
let longitude: Double

var location: CLLocation {
return CLLocation(latitude: latitude, longitude: longitude)
}
}

The user location

var currentUserLatitude: Double = ...
var currentUserLongitude: Double = ...
let userLocaton = CLLocation(
latitude: currentUserLatitude,
longitude: currentUserLongitude
)

And a list of places

let places: [Business] = ...

This is how you sort them

let sortedPlaces = places.sort {
userLocaton.distanceFromLocation($0.0.location) < userLocaton.distanceFromLocation($0.1.location)
}

Or if you prefer the extended notation code

let sortedPlaces = places.sort { (left, right) -> Bool in
userLocaton.distanceFromLocation(left.location) < userLocaton.distanceFromLocation(right.location)
}

How does it work?

The sort method accepts a closure.

Inside this closure you must specify the sorting logic. More specifically, given 2 Business values left and right, if left should be placed before right in the final sorting then the closure returns true. Otherwise false.

Sort array by distance near user location from firebase

This is a complex process, requiring multiple steps. I'll try to explain the steps, but you'll have to do quite some work to turn it into a functioning solution.

Geocoding

First up: an address string like the one you have is not a location. There is no way for a computer to compare two of such strings and reliably know how far apart they are.

So the first thing you'll have to do is to turn the addresses into a more reliable indication of a location, i.e. into a latitude and longitude (a.k.a. lat/lon). The process of turning an address into lat/lon is known as geocoding. It is not really an exact science, but there are plenty of services that are quite good at this geocoding bit.

At the end of this geocoding process you will have a lat/lon combination for each address. That puts the problem back into mathematics, which is a much more exact science.

Geoqueries

Next up you'll need to compare the lat/lon of each address and calculate the distance between them. This is a relatively exact science, if you're willing to ignore inaccuracies near the poles and things like that.

Unfortunately the Firebase Realtime Database can natively only order/filter on a single property. Since a location consists of two properties (latitude and longitude) it can't filter on location without some magic.

Luckily somebody came up with a way to translate lat/lon information into a single string, known as a geohash. For example: the Google office in San Francisco is at lat/lon 37.7900515,-122.3923805, which translate to geohash 9q8yyz. The Googleplex in Mountain View is at lat/lon 37.4219999,-122.0862515, which translates to geohash 9q9hvu.

Unlike the addresses you started with, geohashes are very nicely comparable. To quote the (linked) wikipedia explanation:

nearby places [have] similar prefixes. The longer a shared prefix is, the closer the two places are.

In our two examples above, you can see the the two locations are relatively close to each other because they both start with 9q

There is an open-source library for Firebase called GeoFire that:

  1. makes it easy to store locations (for which you must have the lat/lon) in Firebase as geohashes.
  2. provides querying capabilities so that you can get nodes that are within maximum distance of a location you specify.

I recommend that you check out the iOS version of GeoFire.



Related Topics



Leave a reply



Submit