Code, Code, Revolution!
The iPhone (and now iPad) SDK supports reverse geocoding out of the box. Reverse geocoding means you have a coordinate and want to find out where you are located, for instance, the name of the street you are on. Forward geocoding means you know the name of a location and want to find the coordinates. For reasons not totally clear to me there’s no support for forward geocoding, I’ve read somewhere that it has something to do with license deals.
I’ve started working a little with the iPad simulator and with its large display it makes sense presenting information on a map view. Because I haven’t found a good example/API for forward geocoding I’ve decided to publish my own. There are a few players offering geocoding services, Yahoo, CloudeMade, Tele Atlas and of course Google. Yahoo and Google are both free but I’ve decided on using Google.
This sample iPad application contains a search bar and a large UIMapView. Search results are visualized on the map with a placemark and by clicking the placemark the map will zoom to the viewport returned from the geocoding service. When I started working with the geocoding service it was still in version two, this last week Google launched version three. Version two will be depricated of course but I had already written the parser for version two so I’ve included it as well in my sample. Of course the API works for iPhone as well. Because the API contains quite a lot of code you will find the code inside the sample project at the bottom of this page.
Using BSForwardGeocoder is pretty straight forward. Example below:
- (void) searchBarSearchButtonClicked:(UISearchBar *)theSearchBar {
NSLog(@"Searching for: %@", searchBar.text);
if(forwardGeocoder == nil)
{
forwardGeocoder = [[BSForwardGeocoder alloc] initWithDelegate:self];
}
// Forward geocode!
[forwardGeocoder findLocation:searchBar.text];
}
-(void)forwardGeocoderFoundLocation
{
if(forwardGeocoder.status == G_GEO_SUCCESS)
{
int searchResults = [forwardGeocoder.results count];
// Add placemarks for each result
for(int i = 0; i < searchResults; i++)
{
BSKmlResult *place = [forwardGeocoder.results objectAtIndex:i];
// Add a placemark on the map
CustomPlacemark *placemark = [[CustomPlacemark alloc] initWithRegion:place.coordinateRegion];
placemark.title = place.address;
[mapView addAnnotation:placemark];
NSArray *countryName = [place findAddressComponent:@"country"];
if([countryName count] > 0)
{
NSLog(@"Country: %@", ((BSAddressComponent*)[countryName objectAtIndex:0]).longName );
}
[countryName release];
}
if([forwardGeocoder.results count] == 1)
{
BSKmlResult *place = [forwardGeocoder.results objectAtIndex:0];
// Zoom into the location
[mapView setRegion:place.coordinateRegion animated:TRUE];
}
// Dismiss the keyboard
[searchBar resignFirstResponder];
}
}When search has executed the geocoder contains the result and a status code. The status code is from Google and to support both version 2 and 3 of the API there’s an enum containing response codes. If everything goes well the status should be “G_GEO_SUCCESS” and the “results” property will contain an array of BSKmlResult objects which contain the location information returned for the query. Here’s an example query for my home town Stockholm (Google geocoding service version 3) : http://maps.google.com/maps/api/geocode/xml?address=stockholm&sensor=false.
The details for the service can be found here: http://code.google.com/apis/maps/documentation/geocoding/.
There are multiple arguments you can pass to the search url, you should read the geocoding documentation and check the search url in the application before you implement this in your own app.
BSForwardGeocoder
#import <Foundation/Foundation.h>
#import "BSGoogleV2KmlParser.h"
#import "BSGoogleV3KmlParser.h"
// Enum for geocoding status responses
enum {
G_GEO_SUCCESS = 200,
G_GEO_BAD_REQUEST = 400,
G_GEO_SERVER_ERROR = 500,
G_GEO_MISSING_QUERY = 601,
G_GEO_UNKNOWN_ADDRESS = 602,
G_GEO_UNAVAILABLE_ADDRESS = 603,
G_GEO_UNKNOWN_DIRECTIONS = 604,
G_GEO_BAD_KEY = 610,
G_GEO_TOO_MANY_QUERIES = 620
};
@protocol BSForwardGeocoderDelegate <NSObject>
@required
-(void)forwardGeocoderFoundLocation;
@optional
-(void)forwardGeocoderError:(NSString *)errorMessage;
@end
@interface BSForwardGeocoder : NSObject {
NSString *searchQuery;
NSString *googleAPiKey;
int status;
NSArray *results;
id<BSForwardGeocoderDelegate> delegate;
}
-(id) initWithDelegate:(id<BSForwardGeocoderDelegate>)del;
-(void) findLocation:(NSString *)searchString;
@property (assign) id<BSForwardGeocoderDelegate> delegate;
@property (nonatomic, retain) NSString *searchQuery;
@property (nonatomic, readonly) int status;
@property (nonatomic, retain) NSArray *results;
@endBSKmlResult
The result class is the same for both version 2 and 3 of the service. A big difference in the returned information between the versions is the way address components are returned, in version three more information is returned and therefore I’ve created another class to store the address component information. The properties: countryNameCode, countryName, subAdministrativeAreaName and localityName are for version two only. For version three all address information is stored in the “addressComponents” array (contains BSAddressComponent objects). Because there is really no reason to use version 2 anymore you probably want to remove this code for your own application.
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import "BSAddressComponent.h"
@interface BSKmlResult : NSObject {
NSString *address;
NSString *countryNameCode;
NSString *countryName;
NSString *subAdministrativeAreaName;
NSString *localityName;
float viewportSouthWestLat;
float viewportSouthWestLon;
float viewportNorthEastLat;
float viewportNorthEastLon;
float boundsSouthWestLat;
float boundsSouthWestLon;
float boundsNorthEastLat;
float boundsNorthEastLon;
float latitude;
float longitude;
float height;
NSInteger accuracy;
NSArray *addressComponents;
}
@property (nonatomic, retain) NSString *address;
@property (nonatomic, assign) NSInteger accuracy;
@property (nonatomic, retain) NSString *countryNameCode;
@property (nonatomic, retain) NSString *countryName;
@property (nonatomic, retain) NSString *subAdministrativeAreaName;
@property (nonatomic, retain) NSString *localityName;
@property (nonatomic, retain) NSArray *addressComponents;
@property (nonatomic, assign) float latitude;
@property (nonatomic, assign) float longitude;
@property (nonatomic, assign) float viewportSouthWestLat;
@property (nonatomic, assign) float viewportSouthWestLon;
@property (nonatomic, assign) float viewportNorthEastLat;
@property (nonatomic, assign) float viewportNorthEastLon;
@property (nonatomic, assign) float boundsSouthWestLat;
@property (nonatomic, assign) float boundsSouthWestLon;
@property (nonatomic, assign) float boundsNorthEastLat;
@property (nonatomic, assign) float boundsNorthEastLon;
@property (readonly) CLLocationCoordinate2D coordinate;
@property (readonly) MKCoordinateSpan coordinateSpan;
@property (readonly) MKCoordinateRegion coordinateRegion;
-(NSArray*)findAddressComponent:(NSString*)typeName;
@endTo make it somewhat simple to find address components I’ve added a method that will search for components for you. Using version 3 of the geocoding service you will get the country name using this code:
NSArray *countryName = [BSKmlResultPlace findAddressComponent:@"country"];
if([countryName count] > 0)
{
NSLog(@"Country: %@", ((BSAddressComponent*)[countryName objectAtIndex:0]).longName );
}
[countryName release];There are also properties to make the result simple to use with a MKMapView. The “coordinate” property returns a CLLocationCoordinate2D object, “coordinateSpan” calculates and returns a MKCoordinateSpan object for setting the map viewport. The “coordinateRegion” combines both coordinate and coordinateSpan returning a MKCoordinateRegion object that can be used to directly move your MKMapView to the right place.
#import <Foundation/Foundation.h>
@interface BSAddressComponent : NSObject {
NSString *longName;
NSString *shortName;
NSArray *types;
}
@property (nonatomic, retain) NSString *longName;
@property (nonatomic, retain) NSString *shortName;
@property (nonatomic, retain) NSArray *types;This class maps against what’s returned in version 3 of the geocoding service. It’s just a container class.
You will find the API inside this sample project. For obvious reasons the code hasn’t been tested on the actual device but I’ve verified it through Leaks.
Google forward geocoding API sample iPad application
Happy geocoding!
With this blog I try to provide useful tips and solutions for programming .NET, Objective-C and more. My name is Björn Sållarp, and I love writing code.
Joao Prado Maia
March 15th, 2010 at 4:19 am
Bjorn,
The class looks really interesting, but shouldn’t the functionality be asynchronous?
–Joao
Sijo
March 15th, 2010 at 1:06 pm
nice example…thanks for sharing..
Björn Sållarp
March 15th, 2010 at 8:23 pm
@Joao,
You are right, asynch is much better. I’ve updated the post and sample with asynchronous geocoding. Thanks for your feedback!
// Björn
Armindo
March 17th, 2010 at 5:39 pm
Really great sample, it saves me a lot of time ! Thanks for sharing.
Hope google won’t change too many times their version.
Armindo
March 19th, 2010 at 3:59 pm
Dear Björn,
you have a NSString called “googleApiKey” that is never used. What should I do with it ?
in the source it is not allocated but released, that may cause a problem.
I have also a compiler warning using SDK 3.1.3 here :
if(parseError != nil)
{
if([delegate respondsToSelector:@selector(forwardGeocoderError:)])
{
[delegate performSelectorOnMainThread:@selector(forwardGeocoderError:) withObject:[parseError localizedDescription] waitUntilDone:NO];
}
}
else {
if([delegate respondsToSelector:@selector(forwardGeocoderFoundLocation)])
{
[delegate performSelectorOnMainThread:@selector(forwardGeocoderFoundLocation) withObject:nil waitUntilDone:NO];
}
}
Don’t know if it is ok on SDK 3.2.
on my iPhone device I get a lot of crahes when I close the MapViewController, but everything ok on Simulator
Again, I don’t know if it is related to SDK or not.
Regards
Armindo
Armindo
March 19th, 2010 at 5:00 pm
Dear Björn,
I can confirm you that the crashes come from the warning part.
I can also sometimes get the error in the simulator.
In fact after adding geocoding in one of my app, the main view is not the mapping view. And after shwing/closing a few time the view I have the error.
forwardGeocoder = [[BSForwardGeocoder alloc] initWithDelegate:self]; doesn’t pass a delegate , and it may create a problem later.
Here’s the kind of message I get :
*** NSInvocation: warning: object 0×5730060 of class ‘_NSZombie_GeocodedMapViewController’ does not implement methodSignatureForSelector: — trouble ahead
2010-03-19 16:39:56.747 MyApp[6350:207] *** NSInvocation: warning: object 0×5730060 of class ‘_NSZombie_GeocodedMapViewController’ does not implement doesNotRecognizeSelector: — abort
Hope this help
Armindo
Björn Sållarp
March 20th, 2010 at 10:26 am
@Armindo.
The google key is a leftover from when I started developing, Googles geocoding service doesn’t require a key anymore, not even version 2. You can just remove the google key bits.
On your problem, it’s very hard for me to know what the problem is in your code. are you sure your map view implement the delegate protocol? If you find the problem and it’s related to my code, please post a comment with details and I will update the sample.
Regards,
Björn
Rui Lopes
March 20th, 2010 at 7:24 pm
Hi,
It does not work on iphone 3.1.3.
Erro: performSelectorOnMainThread:withObject:waitUntilDone:’ not found in protocol(s)
Can you help me?
Rui.Lopes@me.com
Armindo
March 21st, 2010 at 10:00 am
Rui,
it’s not really an error but a warning, I have downloaded 3.2 and same problem. and this result in a unstable version. Maybe a casting solve solve the problem. Will look at it and post something if I solve the problem. I’m quite new to iPhone dev.
Regards
Björn Sållarp
March 21st, 2010 at 12:53 pm
Hey guys. I used some sunday time to update the sample code and remove the warnings. I’m sorry I missed the warnings in my first release but the warnings were not really causing problems with the application. I’ve also made some changes in memory management to remove the issued reported by running an analysis.
Oh yeah, I removed the google key bits as well.
// Björn
Armindo
March 21st, 2010 at 2:53 pm
Thanks Björn,
much much better now
Don’t forget to remove the google key in BSForwardGeocoder too.
Have a nice week-end.
Brendt
March 24th, 2010 at 10:16 am
Thanks heaps for this code Björn, it is exactly what I was looking for. Unfortunately I can only get it to run on iPhone Simulator 3.0 but no later SDKs. It runs with no errors or warnings and opening in the simulator but quits straight away.
Any ideas?
Björn Sållarp
March 25th, 2010 at 7:48 am
@Brendt
I used iPhone SDK 3.2 beta5. Does the sample app not run, or did you copy the code into your own project and that fails to run?
Ben
March 26th, 2010 at 2:31 am
Is there a quick adaptation that is needed to make this run on iPhone? I tried it without any modifications (other than setting the SDK to 3.1.3) and it immediately crashes with an invalid region error: ‘Invalid Region ‘
Thanks in advance for assistance. I am downloading the 3.2 beta 5 now to see if the problem is the simulator.
bh
Ben
March 26th, 2010 at 2:32 am
P.S. something edited out the substance of the error message I posted, so here it is with the angle brackets removed: ‘Invalid Region [center:+82.75106922, -90.00000000 span:+170.10225756, +360.00000000]‘
Armindo
March 28th, 2010 at 4:55 pm
Ben,
I run it on iPhone without problem…
nono
April 7th, 2010 at 12:00 pm
Cool job
Deeepak Patel
April 10th, 2010 at 10:25 am
thanks for the wonderful samples. I recently implement it in one of my iPad Apps and I am not able to tap on pin (custom placemark). Is this something I missing ? Also I did not get that How this find current location when app open in iPad. As per my knowledge iPad doesn’t have GPS function and I also didn’t see and corelocation use. Can you please explain me. I would very appreciate if you can help me.
Björn Sållarp
April 10th, 2010 at 11:00 am
@Deeepak
Does nothing happen at all when you tap the placemark? If you are zoomed out and tap a pin it should zoom in to that location. If you are already zoomed in you might not notice that anything happens. In the sample I log when a pin is tapped, check the sample and compare with your app and also check your gdb for log output.
I am not 100% sure how the positioning works in MKMap framework, but you can position a GPS-less device using WiFi depending on if a company called SkyHook scans the area you are in. It is also possible to position a person somewhat accurately from an IP, check any porn site and they will likely show you girls from somewhere close to where you live
. I do know the iPad supports WiFi (SkyHook) positioning because when I got to play around with an iPad at work yesterday it did position me at our old office that we moved out of two months ago.
You can find out if your area is scanned here: http://www.skyhookwireless.com/howitworks/coverage.php , so the bottom line is that you can’t rely on the iPad to find your correct location. I’m sure the 3G model will provide GPS like functionality either by real GPS or by cell tower triangulation.
Good luck with your app!
xiaoke
April 11th, 2010 at 9:02 am
it is what I just looking for. thanks
Deeepak Patel
April 14th, 2010 at 7:00 am
Hi,
Thanks for the response. Here is the explanation.
I load the app and search for the location california. It dropped the pin on Map at California. So now when I single tap on the pin placemark and it doesn’t do anything. I also checked the console for the log but it doesn’t have anything when tapped. If I double tap placemark then it zoom out the map. But I need to show annotation when single tap on the placemark.
I checked in the sample code as well and it also doesn’t do any thing when single tap on placemark.
Please explain me what should I need to do or if I am wrong at any part of the code. I would appreciate your reply.
Thanks & Regards,
Deepak Patel
Deeepak Patel
April 14th, 2010 at 7:02 am
In Addition, You wrote some code for placemark selected in following function.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
But it never calls in the scope of the project whether I tapped on placemark.
Spanish Joe
April 16th, 2010 at 10:48 pm
I found an error in BSGoogleV3KmlParser.m in the name of an XML element
else if ([elementName isEqualToString:@"formattedAddress"])
should be
else if ([elementName isEqualToString:@"formatted_address"])
Thanks for the contribution!
ryan
April 18th, 2010 at 8:59 pm
Thank you very much for posting this. It’s of great help to us! Really appreciate it!
Tom
May 24th, 2010 at 2:46 pm
Thank you very much for this terrific piece of code, this is exactly what I was looking for! Very cool
Anna Billstrom
May 25th, 2010 at 11:23 pm
Thanks Björn, this is just what I was looking or this weekend- we are using historical data with no lat/long and need to do a lookup. Can’t wait to implement, and thanks so much for building.
Matteo
May 26th, 2010 at 2:37 pm
Thanks Bjorn, this is great!
I was looking to implement Google geocoding myself using the JSON-framework but you save a lot of time.
Thanks again and keep up the good job!
Stefan Klumpp
May 27th, 2010 at 11:52 am
Thanks a lot Björn for all the work and effort you put into this and providing it to the public. This saved me an incredible time of work.
The library works perfectly and the sample is very easy to understand.
Nino D'Aversa
May 28th, 2010 at 2:23 am
You sir are awesome!
Thanks for this, going to save me some time writing my own parser.
ND
Oscar
May 28th, 2010 at 10:49 pm
Hello!
Thank you very much for your code and article. I’ve been asking about if google’s geocoding service is free, and someone from google tell me that I have to pay for use.
Are you sure that google’s geocoding service is free?
SM
May 30th, 2010 at 7:38 pm
Thanks for the tutorial! Its a great help.
Björn Sållarp
May 30th, 2010 at 8:45 pm
First of all, I don’t take any responsibility for code posted on this blog. That being said, I havn’t investigated the legal issues using Googles forward geocoding API. They do provide it without the need for a licence key since version 3 and when used together with Google maps in the iphone i can’t see how it would be any different from using the forward geocoding API on the web using the javascript API, which is OK. On the other side, if Google was OK with it, why isn’t it already included in MapKit? It’s obvious that a lot of people want to forward geocode.
Because the API calls are coming from the iphone Google won’t be able to block the request coming from your app. But Google could still hunt you down if you publish your app on the appstore and they get really pissed off.
If you find out what Google has to say about it, please leave a message.
// Björn
noname
June 9th, 2010 at 10:06 am
This is awesome and helped my project a lot. Thank you very much!
Somebody
June 9th, 2010 at 10:12 pm
What’s happening here?
“_OBJC_CLASS_$_BSForwardGeocoder”, referenced from:
objc-class-ref-to-BSForwardGeocoder in RootViewController.o
ld: symbol(s) not found
collect2: ld returned 1 exit status
Björn Sållarp
June 10th, 2010 at 8:45 am
@somebody. I have no idea, which version of Xcode are you using? The sample hasn’t been tested with the latest version release a day or so ago. However, a new xcode version shouldn’t break the sample.
Armindo
June 11th, 2010 at 10:26 am
Hi Björn,
When compiling for IOS 4 it raises a warning in BSGoogleV2KmlParser.h, warning: class ‘BSGoogleV2KmlParser’ does not implement the ‘NSXMLParserDelegate’ protocol.
Regards
Björn Sållarp
June 20th, 2010 at 2:19 pm
@Armindo,
Don’t worry about the V2-parser. You can exclude that from your project. Google released V3 quite a while ago so you should use the new API instead of the old.
Peter Nash
June 21st, 2010 at 12:36 pm
Hi Björn,
Great stuff and thanks for sharing. I thought about using Google’s JSON response instead of the XML parser as it’s quicker to download and parse – why did you choose XML?
Thanks
Peter
Greenflame
July 5th, 2010 at 1:31 am
Hey Björn!
Thanks so much for this tutorial, and for the sample code! This is exactly what I needed! Keep up the good work!
<3
Fauad Anwar
July 13th, 2010 at 8:00 am
Hi,
I am confused on topic about key.
I am developing an application which show current location and info near about my location.
Is it require for me to obtain key for it?
When i run application in simulator it point my location to some infinite loop place not my location. (I am using Interface builder to show my current location). Is this due to simulation and it will work properly on iPhone??
Also how can i get traffic info from Google and show it in my map?
If possible give reply or give some sample code.
(last time i have created a API key, it works for my app. that time. But same code not working today for me.) Giving me error invalid parameter where i called url with my key.
Thanks in Advance.
Björn Sållarp
July 17th, 2010 at 10:23 am
@Fauad,
The example in this post is only for forward geocoding, meaning, you know the name of a location and want the coordinates of that location. Google maps API v3 doesn’t require an API key but other services do, you probably want to use the new Places API (still in preview). Check out Googles services here:
http://code.google.com/intl/sv-SE/apis/maps/documentation/webservices/index.html
User location in the simulator only shows a somewhat correct location if you’re on a WLAN that has been scanned by Skyhook, otherwise your location is set to California USA. This will work on the iPhone.
You can find more information on traffic information here:
http://code.google.com/intl/en-US/apis/maps/documentation/javascript/overlays.html#TrafficLayer
I’m not sure if you can use it for the iphone.
Good luck with your project!
Gerald
July 22nd, 2010 at 7:35 pm
Thank you very much. This awesome.
Gerald
Gonso
July 29th, 2010 at 1:26 am
Great Implementation!
I’ve added this to a project and its working like a charm!
Hi quality work.
Gonso