Introduction
Last week I started to familiarize myself with the ESRI ArcGIS iPhone API public beta and I blogged about my First Impressions. This week I had a chance to play around with it some more and I decided to investigate working with location. I enhanced my sample application from last week to include a Location button, which when pressed zooms the map to the current location and displays a custom push pin graphic.
Click here to download the source code from this article.
There are two ways you can work with location while writing an app with ESRI’s iPhone API:
ESRI’s
AGSGPS class provides a convenient wrapper around Core Location if you don’t want (or need) to delve into the details of the framework.
[more]
ESRI AGSGPS Class vs. Apple Core Location Framework
Here’s how the ESRI iPhone API documentation describes the AGSGPS class:
“This object controls how the map responds to GPS input. To make the map start responding to GPS input, call the start method. The map will automatically zoom to the first location specified by the GPS input. You can control the zoom level by specifying the zoomLevel property. If the autoPan property is enabled, the map will recenter everytime a new GPS location is received. To make the map stop responding to GPS input, call the stop method. By default, the map uses a round, blue symbol to display the current location. You can replace this symbol with an image icon of your choice. This image must be included in the application bundle, it must be named GpsDisplay.png and it must be 35x35 pixels in size.”
The class definitely simplifies working with Core Location, but the trade off is that you are somewhat limited in what you can do. For example, Core Location includes the CLLocationManagerDelegate protocol which is “used to receive location and heading updates” so you can respond to them in your code. I’d like to see something similar in the ESRI iPhone SDK to give us more flexibility to respond to location updates. With the ESRI iPhone SDK, you just turn on the GPS and let the map do it’s thing.
For my sample app, I decided to work with the Core Location framework for finer grained control over the user interaction.
Implementation Details
The first step I took was to rearrange the UI in Interface Builder to accommodate the new button. I decided to add a proper toolbar along the bottom and center the new button and existing segment control. This required adding Flexible Bar Button Items on each side of the toolbar, as shown:
Finding the proper icon for the Location button was a bit tricky. Interface Builder doesn’t give you a way to specify this particular system icon for your button. I had to follow Diallo’s advice from this Stack Overflow question to get the icon from the UI Kit and save it to disk. I temporarily added this code to the viewDidLoad event, and ran the application once to write the icon out to my document directory.
UIImage* img = [UIImage kitImageNamed:@"UIButtonBarLocate.png"];
// Get the location of the Documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) ;
NSString *imagePath = [paths objectAtIndex:0] ;
NSString *filename = @"Locate.png" ;
NSString *filepath = [NSString stringWithFormat:@"%@/%@", imagePath, filename] ;
// Save the image
NSData *imageData = [NSData dataWithData:UIImagePNGRepresentation(img)];
[imageData writeToFile:filepath atomically:YES];
If your not sure where the file gets saved, you can add an NSLog call to output imagePath to the Console. After the icon was saved, I removed the temporary code and copied the icon into the project directory, under the Resources group.
Once I had the view setup properly in interface builder, I modified the view controller’s header file to:
- Import Core Location
- Conform to the CLLocationManagerDelegate protocol
- Declare members and properties for the location manager and locate button
- Declare an IBAction method to run when the user clicks the locate button
I also cleaned up the previous week’s code a bit, to remove unnecessary AGSTiledMapServiceLayer members and properties.
Here is the new version of MyFirstMapAppViewController.h:
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
#import "AGSiPhone.h"
#define kTiledStreetMapServiceURL @"http://server.arcgisonline.com/ArcGIS/rest/services/ESRI_StreetMap_World_2D/MapServer"
#define kTiledImageryMapServiceURL @"http://server.arcgisonline.com/ArcGIS/rest/services/ESRI_Imagery_World_2D/MapServer"
#define kTiledReliefMapServiceURL @"http://server.arcgisonline.com/ArcGIS/rest/services/ESRI_ShadedRelief_World_2D/MapServer"
@interface MyFirstMapAppViewController : UIViewController<AGSMapViewDelegate, CLLocationManagerDelegate> {
AGSMapView *mapView;
CLLocationManager *locationManager;
UIView *streetView;
UIView *imageryView;
UIView *reliefView;
UIBarButtonItem *locateButton;
}
@property (nonatomic, retain) IBOutlet AGSMapView *mapView;
@property (nonatomic, retain) CLLocationManager *locationManager;
@property (nonatomic, retain) UIView *streetView;
@property (nonatomic, retain) UIView *imageryView;
@property (nonatomic, retain) UIView *reliefView;
@property (nonatomic, retain) IBOutlet UIBarButtonItem *locateButton;
- (IBAction)toggleLayer:(id)sender;
- (IBAction)showLocation:(id)sender;
@end
For the implementation file, Core Location is used in the following manner:
- The location manager is created and started in viewWillAppear
- The locate button is disabled in viewWillAppear to ensure that the button can’t be used until Core Location services have become available
- A new graphics layer is created and added to the map in viewDidLoad
- The location button is enabled in the location manager’s didUpdateToLocation event once core location is started and ready to use
- When the user touches the Locate button, the showLocation method gets the location, creates and zooms to an envelope around that location, and displays a push pin graphic in the graphics layer
- The location manager is shut down in viewWillDisappear
This code follows the same basic pattern shown in one of the examples in Head First iPhone Development.
Here is the MyFirstMapAppViewController.m file:
#import "MyFirstMapAppViewController.h"
@implementation MyFirstMapAppViewController
@synthesize mapView;
@synthesize streetView;
@synthesize imageryView;
@synthesize reliefView;
@synthesize locationManager;
@synthesize locateButton;
- (void)viewWillAppear:(BOOL)animated {
// Setup location manager
NSLog(@"Starting core location");
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
self.locateButton.enabled = NO;
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
self.mapView.mapViewDelegate = self;
AGSTiledMapServiceLayer *streetLayer = [[AGSTiledMapServiceLayer alloc]
initWithURL:[NSURL URLWithString:kTiledStreetMapServiceURL]];
self.streetView = [self.mapView addMapLayer:streetLayer withName:@"Street"];
[streetLayer release];
AGSTiledMapServiceLayer *imageryLayer = [[AGSTiledMapServiceLayer alloc]
initWithURL:[NSURL URLWithString:kTiledImageryMapServiceURL]];
self.imageryView = [self.mapView addMapLayer:imageryLayer withName:@"Imagery"];
[imageryLayer release];
AGSTiledMapServiceLayer *reliefLayer = [[AGSTiledMapServiceLayer alloc]
initWithURL:[NSURL URLWithString:kTiledReliefMapServiceURL]];
self.reliefView = [self.mapView addMapLayer:reliefLayer withName:@"Relief"];
[reliefLayer release];
self.streetView.hidden = NO;
self.imageryView.hidden = YES;
self.reliefView.hidden = YES;
// Create a graphics layer to display the push pin
AGSGraphicsLayer *graphicsLayer = [AGSGraphicsLayer graphicsLayer];
[self.mapView addMapLayer:graphicsLayer withName:@"GraphicsLayer"];
}
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return YES;
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
NSLog(@"Shutting down core location");
[self.locationManager stopUpdatingLocation];
self.locationManager = nil;
}
- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)dealloc {
self.mapView = nil;
self.streetView = nil;
self.imageryView = nil;
self.reliefView = nil;
[locateButton release];
[super dealloc];
}
- (IBAction)toggleLayer:(id)sender {
self.streetView.hidden = (((UISegmentedControl *)sender).selectedSegmentIndex != 0);
self.imageryView.hidden = (((UISegmentedControl *)sender).selectedSegmentIndex != 1);
self.reliefView.hidden = (((UISegmentedControl *)sender).selectedSegmentIndex != 2);
}
- (IBAction)showLocation:(id)sender {
NSLog(@"Show Location");
CLLocation *location = self.locationManager.location;
double lat = location.coordinate.latitude;
double lon = location.coordinate.longitude;
NSLog(@"%.3f, %.3f", lat, lon);
double size = 0.05;
AGSEnvelope *envelope = [AGSEnvelope envelopeWithXmin:lon - size
ymin:lat - size
xmax:lon + size
ymax:lat + size
spatialReference:self.mapView.spatialReference];
[self.mapView zoomToEnvelope:envelope animated:YES];
// Get reference to the graphics layer
id<AGSLayerView> graphicsLayerView = [self.mapView.mapLayerViews objectForKey:@"GraphicsLayer"];
AGSGraphicsLayer *graphicsLayer = (AGSGraphicsLayer*)graphicsLayerView.agsLayer;
// Clear graphics
[graphicsLayer removeAllGraphics];
// Create a marker symbol using the Location.png graphic
AGSPictureMarkerSymbol *markerSymbol = [AGSPictureMarkerSymbol pictureMarkerSymbolWithImageNamed:@"Location.png"];
// Create a new graphic using the location and marker symbol
AGSGraphic* graphic = [AGSGraphic graphicWithGeometry:[envelope center]
symbol:markerSymbol
attributes:nil
infoTemplate:nil];
// Add the graphic to the graphics layer
[graphicsLayer addGraphic:graphic];
}
- (void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
NSLog(@"Core location has a new position");
self.locateButton.enabled = YES;
}
- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
NSLog(@"Core location failed to get position");
self.locateButton.enabled = NO;
}
#pragma mark AGSMapViewDelegate
//called when the map view is loaded (after the view is loaded)
- (void)mapViewDidLoad:(AGSMapView *)mapView {
//create extent to be used as default
AGSEnvelope *envelope = [AGSEnvelope envelopeWithXmin:-124.83145667
ymin:30.49849464
xmax:-113.91375495
ymax:44.69150688
spatialReference:mapView.spatialReference];
//call method to set extent, pass in envelope
[self.mapView performSelector:@selector(zoomToEnvelope:animated:)
withObject:envelope
afterDelay:0.5];
}
@end
Summary
In this post we looked at some options for utilizing Core Location with the ESRI ArcGIS iPhone API. ESRI’s AGSGPS class provides an easy to use convenience wrapper around Core Location, but it is not as flexible as working directly with Apple’s Core Location framework. An example was presented that demonstrates how to work with the Core Location framework and ESRI’s iPhone API. The example showed how to get the current location and zoom to it while showing a custom graphic on the map.
Click here to download the source code from this article.
I’m having a lot of fun learning about iPhone programming and ESRI’s iPhone API. I addition to testing in the simulator, I signed up for the Apple Developer Program so I can now run these apps on my iPod Touch. (The Mac Mini in the background is my dev box!)
I hope you are enjoying these posts and finding the information helpful. If you have any suggestions, ideas or feedback, please leave them in the comments below.
Additional Resources