Can't loop geocoding? - ios

At the moment I'm working on an application for the iPad, which at a certain point should show a list of cities with next to these cities the distance from the users current location.
I'm using a loop which uses a database I built earlier. This is the code:
NSString *address = [NSString stringWithFormat:#"%#, Nederland", [info2 objectForKey:#"stadsnaam"]];
[self.geocoder geocodeAddressString:address completionHandler:^(NSArray *placemarks, NSError *error) {
CLPlacemark *placemark = [placemarks objectAtIndex:0];
CLLocation *location = placemark.location;
CLLocationCoordinate2D firstLocation = location.coordinate;
NSLog(#"%f,%f", firstLocation.latitude, firstLocation.longitude);
}];
This piece of code is inside a for loop. But the first time it is run, it just returns 0.0000
The second time the loop is activated (and I'm sure it runs more than one time) it returns only the distance between the current location and the FIRST city.
The app gets the city names from this piece of code:[NSString stringWithFormat:#"%#, Nederland", [info2 objectForKey:#"stadsnaam"]]
Which I verified and works. I would like to avoid using the Google API because I will be making more than the maximum request a day probably XD
Any help is appreciated, please inform me if anything isn't completely clear and thanks in advance!

You need to read the documentation for -geocodeAddressString:completionHandler::
After initiating a forward-geocoding request, do not attempt to initiate another forward- or reverse-geocoding request.
You can use CLGeocoder's geocoding property to determine whether a geocoding operation is in progress.

Related

for (id obj in studentArray) only iterating through first item in array

The below is suppose to run through a NSMutableArray of *Students with an address property. But for some reason the for isn't iterating through the entire NSMutableArray, only the first object.
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
for (id obj in studentArray) {
if ([obj isKindOfClass:[Student class]]) {
Student *selectedStudent = obj;
[geocoder geocodeAddressString:selectedStudent.address completionHandler:^(NSArray* placemarks, NSError* error){
for (CLPlacemark* aPlacemark in placemarks)
{
NSString *latDest1 = [NSString stringWithFormat:#"%.4f",aPlacemark.location.coordinate.latitude];
NSString *lngDest1 = [NSString stringWithFormat:#"%.4f",aPlacemark.location.coordinate.longitude];
NSLog(#"%#, %#",latDest1, lngDest1);
}
}];
}
}
Any Ideas what I could be doing wrong? The app doesn't crash, just simply checks one of 4 students in the array.
Thanks
Looks like geocoding does not return placemarks for the next Student objects. The problem may be because of too much of geocode requests.
From the Apple documentation about Geocoding:
After initiating a forward-geocoding request, do not attempt to
initiate another forward- or reverse-geocoding request. Geocoding
requests are rate-limited for each app, so making too many requests in
a short period of time may cause some of the requests to fail. When
the maximum rate is exceeded, the geocoder passes an error object with
the value network to your completion handler.

CLPlacemark country property is null

the latest Xcode/SDK iOS download is no longer providing the 'country' string.
- (void)GEOLocator
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLLocation *myLocation = [[CLLocation alloc] initWithLatitude:currentCentre.latitude longitude:currentCentre.longitude];
[geocoder reverseGeocodeLocation:myLocation completionHandler:^(NSArray *placemarks, NSError *error)
{
// the returned error code is 0
NSLog(#"%ld",(long)error.code);
// there’s only one entry in the placemarks NSArray
NSLog(#"placemarks count(%lu)",(unsigned long)[placemarks count]);
CLPlacemark *placemark = [placemarks firstObject];
// and the country property is null
NSLog(#"placemark.country(%#)",placemark.country);
}
];
}
currentCentre.latitude and currentCentre.longitude is hardcoded to downtown San Francisco: these have been proven to work.
the NSLog output’s are:
2015-08-29 15:47:48.299 MyApp[10128:548448] 0
2015-08-29 15:47:48.299 MyApp[10128:548448] placemarks count(1)
2015-08-29 15:47:48.300 MyApp[10128:548448] placemark.country((null))
this code sequence is about as simple as it gets, yet the latest Xcode/iOS download no longer can tell me what country I’m in?!
if this isn't correct can someone please post the correct way to retrieve 'county' from CLLocation?
the core problem is that in Xcode/Simulator you can set lat/long values in multiple places begging the question: what happens when these settings conflict?
the simulator had me out in the middle of the North Atlantic! which explains why the 'country' (as well as several others) were null. the difficulty lie in setting a test lat/long value and the fact one can do that in multiple places: 1) Edit Scheme, 2) the Simulator Loction menu item, and 3) directly in source.
it would be nice to set test lat/long's in one place and everything then just works.

How to check my current location against locations in a remote file - iOS app

I have a simple locations map and I want to make my app beep when the user is approaching a location that is listed in a remote file
the listed locations are on my server named locations.txt
how can i check locations.txt every 1 minute to see if the user is within 300m of a location??
The standard answer to this question is Shape-Based Regions as described in the Location Awareness Guide. Generally, shape-based regions is the way to go if you have a limited number of regions. But, given that you want a lot of regions, you might have to "roll your own":
Turn on a location service and monitor your location. See the Location Awareness Programming Guide. If you use standard location service, make sure to set a desiredAccuracy that is as low as possible to achieve the functional need (e.g. kCLLocationAccuracyHundredMeters).
Once you've successfully received the first didUpdateLocations, if you really want to check every minute, you could create a timer at that point. If the purpose of that timer is to just check the users' location, then the timer is really not needed and you can just wait for occurrences of the didUpdateLocations.
You can iterate through your array of locations to monitor (I'd convert them to CLLocation objects) and simply use distanceFromLocation.
A couple of observations, though:
You suggest that you want to check locations.txt every minute to see if user is within 300m of location. I can imagine two reasons why you might have proposed that solution:
Did server's locations.txt change? If this is the problem you're trying to solve, a better solution would be push notifications (a.k.a. "remote notifications") and you want to make sure the client has access to the latest information. The process of constantly re-retrieving the file is very expensive (in terms of bandwidth, battery, computationally); or
Did the user move? If you're concerned about whether the user may have moved, the right solution is not to check every minute, but rather wait for the [CLLocationManagerDelegate] instance method didUpdateLocations to be called. If you want to avoid too many redundant checks, you can always keep track of whether the last request took place less than a minute ago or not, and then only check again if it was more than a minute ago. But that's very different than checking every minute whether you need to or not.
You've suggested using a text file. You might want to contemplate using a JSON file (or XML file), which is a better mechanism for retrieving data from a server.
For example, if you have a text file in JSON format, you can parse the results in another single line of code (JSONObjectWithData). To illustrate, let me show you what a JSON file might look like (where the square brackets designate an array, and the curly braces designate a dictionary, this is therefore an array of dictionaries):
[
{
"name" : "Battery Park",
"latitude" : 40.702,
"longitude" : -74.015
},
{
"name" : "Grand Central Station",
"latitude" : 40.753,
"longitude" : -73.977
}
]
Then your app can retrieve the results incredibly easily with two lines:
NSData *locationsData = [NSData dataWithContentsOfURL:url];
NSArray *locationsArray = [NSJSONSerialization JSONObjectWithData:locationsData options:0 error:&error];
So, you'll need to start location services:
if (nil == self.locationManager)
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
// Set a movement threshold for new events.
self.locationManager.distanceFilter = 500;
[self.locationManager startUpdatingLocation];
You'll then have a routine for checking the current location:
- (void)checkLocation
{
NSURL *url = [NSURL URLWithString:kLocationsUrlString];
NSData *locationsData = [NSData dataWithContentsOfURL:url];
NSAssert(locationsData, #"failure to download data"); // replace this with graceful error handling
NSError *error;
NSArray *locationsArray = [NSJSONSerialization JSONObjectWithData:locationsData
options:0
error:&error];
NSAssert(locationsArray, #"failure to parse JSON"); // replace with with graceful error handling
for (NSDictionary *locationEntry in locationsArray)
{
NSNumber *longitude = locationEntry[#"longitude"];
NSNumber *latitude = locationEntry[#"latitude"];
NSString *locationName = locationEntry[#"name"];
CLLocation *location = [[CLLocation alloc] initWithLatitude:[latitude doubleValue]
longitude:[longitude doubleValue]];
NSAssert(location, #"failure to create location");
CLLocationDistance distance = [location distanceFromLocation:self.locationManager.location];
if (distance <= 300)
{
NSLog(#"You are within 300 meters (actually %.0f meters) of %#", distance, locationName);
}
else
{
NSLog(#"You are not within 300 meters (actually %.0f meters) of %#", distance, locationName);
}
}
}
And this will be called when the user's location changes:
// this is used in iOS 6 and later
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
[self checkLocation];
}
// this is used in iOS 5 and earlier
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 6.0)
[self checkLocation];
}
The implementation might look like this test project on GitHub. This is a barebones implementation, but it gives you an idea of the tools you have at hand, namely retrieving your locations.json file and comparing that to the location retrieved by the device.

How to convert more than one place names into coordinates?

I'm trying to forward geocode 2 place names into coordinates with following code:
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:place
inRegion:nil
completionHandler:^(NSArray* placemarks, NSError* error){
NSLog(#"a");
NSLog(#"count %d", [placemarks count]);
for (CLPlacemark* aPlacemark in placemarks) {
CLLocationCoordinate2D coord = aPlacemark.location.coordinate;
NSLog(#"%f, %f", coord.latitude, coord.longitude);
}
}];
[geocoder geocodeAddressString:place
inRegion:nil
completionHandler:^(NSArray* placemarks, NSError* error){
NSLog(#"b");
NSLog(#"count %d", [placemarks count]);
for (CLPlacemark* aPlacemark in placemarks) {
CLLocationCoordinate2D coord = aPlacemark.location.coordinate;
NSLog(#"%f, %f", coord.latitude, coord.longitude);
}
}];
To simplify, I convert a single place name twice. When I run the code, only the first geocoding completion handler is runed. Remaining geocoding completion handlers are ignored.
I'd like to know why does it happen and how to convert more than one places.
See Apple's guidance:
http://developer.apple.com/library/ios/#documentation/CoreLocation/Reference/CLGeocoder_class/Reference/Reference.html
Applications should be conscious of how they use geocoding. Here are
some rules of thumb for using this class effectively:
1) Send at most one geocoding request for any one user action.
2) If the user performs multiple actions that involve geocoding the
same location, reuse the results from the initial geocoding request
instead of starting individual requests for each action.
3) When you want to update the user’s current location automatically
(such as when the user is moving), issue new geocoding requests only
when the user has moved a significant distance and after a reasonable
amount of time has passed. For example, in a typical situation, you
should not send more than one geocoding request per minute.
4) Do not start a geocoding request at a time when the user will not
see the results immediately. For example, do not start a request if
your application is inactive or in the background.
You're not supposed to do more than one geocoding operation at a time. The block happens asynchronously, so the second geocoding operation will probably start before the first one has a chance to finish. Here's the docs:
This method submits the specified location data to the geocoding server asynchronously and returns. Your completion handler block will be executed on the main thread. After initiating a forward-geocoding request, do not attempt to initiate another forward- or reverse-geocoding request.

CLGeocoder reverseGeocodeLocation returns 'kCLErrorDomain error 9'

I'm developing an iOS app with reverse geocoding features according to this article: geocoding tutorial
But when I test like this on simulator, I get 'kCLErrorDomain error 9'. I've searched a lot and there are only error 0 or 1 not 9.
Here is my code in viewDidLoad:
self.locationManager = [[CLLocationManager alloc]init];
self.locationManager.delegate = self;
self.locationManager.distanceFilter = 80.0;
[self.locationManager startUpdatingLocation];
CLGeocoder *geocoder = [[[CLGeocoder alloc] init] autorelease];
[geocoder reverseGeocodeLocation:self.locationManager.location
completionHandler:^(NSArray *placemarks, NSError *error) {
NSLog(#"reverseGeocodeLocation:completionHandler: Completion Handler called!");
if (error){
NSLog(#"Geocode failed with error: %#", error);
return;
}
if(placemarks && placemarks.count > 0)
{
//do something
CLPlacemark *topResult = [placemarks objectAtIndex:0];
NSString *addressTxt = [NSString stringWithFormat:#"%# %#,%# %#",
[topResult subThoroughfare],[topResult thoroughfare],
[topResult locality], [topResult administrativeArea]];
NSLog(#"%#",addressTxt);
}
}];
Thank you very much.
The Core Location error codes are documented here.
Code values 8, 9, and 10 are:
kCLErrorGeocodeFoundNoResult,
kCLErrorGeocodeFoundPartialResult,
kCLErrorGeocodeCanceled
Based on the code shown, the most likely reason you'd get error 8 is that it's trying to use location immediately after calling startUpdatingLocation at which time it might still be nil.
It usually takes a few seconds to obtain the current location and it will most likely be nil until then (resulting in geocode error 8 or kCLErrorGeocodeFoundNoResult). I'm not sure what error code 9 means by "FoundPartialResult" but Apple's GeocoderDemo sample app treats both the same way (as "No Result").
Try moving the geocoding code (all the code after the startUpdatingLocation call) to the delegate method locationManager:didUpdateToLocation:fromLocation:. The location manager will call that delegate method when it actually has a location and only then is it safe to use location.
There, after the geocoding is successful (or not), you may want to call stopUpdatingLocation otherwise it will try geocoding every time the user location is updated.
You may also want to check the accuracy (newLocation.horizontalAccuracy) and age (newLocation.timestamp) of the received location before trying to geocode it.
It turns out I mixed up the longitute and latitude when creating the location. Thought I'd add this as something for people to check.

Resources