CloudKit - distanceToLocation:fromLocation

Is there an issue with distanceToLocation:fromLocation CloudKit predicate?


I am trying to fetch some results from cloud db using the following code:


CGFloat radius = 1000.00; // in meters
CLLocation *currentLocation =
[[CLLocation alloc] initWithLatitude:42.713834 longitude:23.264086];
   
CKLocationSortDescriptor *sortDescriptor =
[[CKLocationSortDescriptor alloc] initWithKey:@"location" relativeLocation:currentLocation];
   
[NSPredicate predicateWithFormat:@"distanceToLocation:fromLocation:(location, %@) < %f",
currentLocation, radius];

I am expecting four records but result contains seven. Three of them exceed my predicate rule.


Result:

lat:42.713402 long:23.275190 distance:910.919712

lat:42.710781 long:23.271160 distance:671.444867

lat:42.734921 long:23.247850 distance:2693.570884

lat:42.724411 long:23.281340 distance:1837.812711

lat:42.721882 long:23.270849 distance:1051.719189

lat:42.714920 long:23.263790 distance:123.055994

lat:42.715111 long:23.261801 distance:234.863830


Distance in the result above was measured for every record with:

[[record objectForKey:@"location"] distanceFromLocation:currentLocation];


Sort descriptor also does not work properly.

By documentation sort descriptor should sort results in ascending order - but it is not true

https://developer.apple.com/library/prerelease/ios/documentation/CloudKit/Reference/CKLocationSortDescriptor_class/index.html#//apple_ref/occ/instm/CKLocationSortDescriptor/initWithKey:relativeLocation:

Replies

I think the parameter for the radius needs to be a `NSNumber`


This is working in my project:

// setup predicate
let distance = NSNumber(integer: 100)
let location = ...
let predicate = NSPredicate(format: "distanceToLocation:fromLocation:(location, %@) < %@", location, distance)

I too had a problem, and then I looked at the CloudKitAtlas sample and noticed an interesting variable name in AAPLCloudManager.m:


CGFloat radiusInKilometers = 5;
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"distanceToLocation:fromLocation:(location, %@) < %f", location, radiusInKilometers];

Convert to Kilometers, not Meters when using this function 😉


Also note that you cannot use <= for some reason.


Hope this helps.

and did you include:

query.sortDescriptors=[NSArray arrayWithObject:sortDescriptior];

I tried the NSnumber, CGFloat, etc... It does not narrow the search?

i use CLLocationDistance this works for me

Your code needs to change this line:

[NSPredicate predicateWithFormat:@"distanceToLocation:fromLocation:(location, %@) < %f", currentLocation, radius];

to:

NSPredicate *myPredicate= [NSPredicate predicateWithFormat:@"distanceToLocation:fromLocation:(location, %@) < %f", currentLocation, radius];

CKQuery *query=[[CKQuery alloc] initWithRecordType:@"MyType" predicate:myPredicate];

query.sortDescriptors =[NSArray arrayWithObject:sortDescriptor];

And, believe it or not, the location sorts from a CloudKit database get more 'accurate' with time - honestly, mine were giving ordering errors last night and tonight they work perfectly. Also, it seems to be filtering based on meters not kilometers.

I have these lines.

The code above was just descriptive.


NSPredicate *predicate = [NSPredicate predicateWithFormat:@"distanceToLocation:fromLocation:(location, %@) < %f",  currentLocation, radius];
CKQuery *query = [[CKQuery alloc] initWithRecordType:@"MyRecord" predicate:predicate];


CKLocationSortDescriptor *sortDescriptor =  [[CKLocationSortDescriptor alloc] initWithKey:@"location" relativeLocation:currentLocation];
[query setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];


Here is my last test.

The radius is 5000m passed to the predicate as CGFloat. Only 12 record match my rule. Lines with distance > 5000m are extra data that I don't need.

I filter these lines using distanceFromLocation, but this is extra work.


lat:42.713402 long:23.275190 distance:910.919712
lat:42.710781 long:23.271160 distance:671.444867
lat:42.734921 long:23.247850 distance:2693.570884
lat:42.724411 long:23.281340 distance:1837.812711
lat:42.721882 long:23.270849 distance:1051.719189
lat:42.714920 long:23.263790 distance:123.055994
lat:42.715111 long:23.261801 distance:234.863830
lat:42.712818 long:23.239189 distance:2042.611038
lat:42.695461 long:23.295670 distance:3295.681649
lat:42.699039 long:23.315130 distance:4493.304914
lat:42.714500 long:23.326611 distance:5122.417117
lat:42.698200 long:23.329470 distance:5631.289302
lat:42.704750 long:23.328791 distance:5396.354962
lat:42.697800 long:23.325090 distance:5305.933690
lat:42.723591 long:23.303680 distance:3419.570719
lat:42.716091 long:23.296970 distance:2705.267491
lat:42.750198 long:23.225439 distance:5131.823377
lat:42.688309 long:23.319748 distance:5370.225749
lat:42.689999 long:23.331470 distance:6123.168271
lat:42.687983 long:23.320285 distance:5426.818538
lat:42.688380 long:23.319387 distance:5340.977640
lat:42.652191 long:23.280630 distance:6980.647302
lat:42.659561 long:23.264830 distance:6029.295483
lat:42.658180 long:23.270250 distance:6202.995351
lat:42.650871 long:23.289980 distance:7309.198664
lat:42.652679 long:23.265970 distance:6795.204816
lat:42.759720 long:23.267130 distance:5103.439966
lat:42.651409 long:23.220289 distance:7808.524973
lat:42.650742 long:23.217840 distance:7967.967831


As you can see, the results are somehow ordered, but not ideal.

Yes, I got the same results. I ended up setting a radius so that the number that comes back is reasonable and then doing this simple sort:

CLLocation *home=[[CLLocation alloc] initWithLatitude:homeLatitude longitude:homeLongitude];

NSArray *theSortedReuslts=[theResults sortedArrayUsingComparator:^(id obj1, id obj2){

NSDictionary *object1=obj1;

NSDictionary *object2=obj2;

if([[object1 objectForKey:@"HomeLocation"] distanceFromLocation:home1]>

[[object2 objectForKey:@"HomeLocation"] distanceFromLocation:home1]){

return (NSComparisonResult)NSOrderedDescending;

}else{

return NSOrderedAscending;

}

}];

Anyone had any luck with this?


I'm still having to post sort the results becaus the sort descriptor doesn't work, even after months of development (so the indexes should be mature).


The results are also becoming increasingly slow. There's less than a thousand rows in the table, but I'm now starting to get multi second waits for results, with it sometimes taking upwards of 10 seconds. Something is wrong.


Obviously I do have the location field indexed both for querying and sorting.

Oh yeah? This listing is in the CKQuery Class Reference.


To test whether two locations are near each other, create a predicate using the distanceToLocation:fromLocation: function as shown in Listing 7. Predicates that use this function must have the structure shown in the listing. In your code, replace the location variable with a field name from one of your records. (This data type for the field must be a CLLocation object.) Similarly, replace the fixedLoc and radius values with appropriate values from your app. The fixedLoc value is the geographic coordinate that marks the center of a circle with the specified radius. In this example, the predicate returns a match if the location in the record is within 10 kilometers of the specified latitude and longitude.


Listing 7Matching by distance from a location

  1. CLLocation* fixedLoc = [[CLLocation alloc] initWithLatitude:37.331913 longitude:-122.030210];
  2. CGFloat radius = 10000;
  3. NSPredicate predicate = [NSPredicate predicateWithFormat:@"distanceToLocation:fromLocation:(location, %@) < %f", fixedLoc, radius]];

There is a question as to whether the radius is in meters or kilometers. Also, the thing doesn't work as it gives results for records outside of the search radius. Me thinks that it's broken.

You shouldn't have to do the extra work and adjust the radius. It needs to be fixed on Apple's end.

Yes I agree that it is broken.


The docs are clear that the radius is in metres. But it appears that the server is taking the given value only as a hint, and is internally using a wider radius than the given. I suspect this could be an index optimisation, in that it the database doesn't have distances indexed down to tens or hundreds of metres, and instead is only granular to 1000s of metres. Thus you will most likely receive results that are outside of your given metres limit.


Also the CKLocationSortDescriptor doesn't work at all. Or at least it didn't last time I checked, some months ago. My code and database have been live and active for many months, but the sorting didn't appear to have improved (or indeed be doing anything at all) when I last checked.


So yeah, that's my conclusion. The CKLocationSortDescriptor doesn't work at all, and while the distanceToLocation predicate is meant to take metres (as the docs state), it is only capable of granularity at the kilometres level. So if you request a 500 metre radius you're going to receive results that are potentially several kilometres outside of that radius.


The upshot is that you need to test the distance of each result and discard the undesired results, and you need to apply your own sort to the results (though I've personally left the CKLocationSortDescriptor code in, in the vain hope that someday it'll start working).

I submitted a Bug Report on this.