The iPhone has plenty of neat features to use, one of the more recent features is using the built in Google Maps support. The specific SDK library we are looking at is MapKit.
The application we are looking at building today is going to display the last 300 earthquakes from around the world - data pulled from USGS. The events are shown on the map, with larger earthquakes being displayed larger in size and a darker color. This shows off putting custom annotations on the map with a custom drawn view for each. Below is a video of the application in action. When it loads it will request the data and update the map.
To get moving on this application, I setup a basic View Based Application (named EarthquakeMap in my case), opened the view nib in Interface Builder and dropped a Map View on it. Back in XCode we need to add a reference to MapKit.framework, which should show up by default in the list to the right - for reference
/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator3.0.sdk/
System/Library/Frameworks
. This completes all the quick setup.Diving into the code, we need to add to our view controller header an IBOutlet for the map view (typed
MKMapView
) - don't forget to add an import for <MapKit/MapKit.h>
. MKMapView
provides a delegate protocol, therefore at this point we declare that our view controller will implement some of the delegate, MKMapViewDelegate
. To finish the header file we add an NSMutableArray
to hold our collection of seismic events that we build from the data we get.#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface EarthquakeMapViewController : UIViewController <MKMapViewDelegate> {
IBOutlet MKMapView *mapView;
NSMutableArray *eventPoints;
}
@end
#import <MapKit/MapKit.h>
@interface EarthquakeMapViewController : UIViewController <MKMapViewDelegate> {
IBOutlet MKMapView *mapView;
NSMutableArray *eventPoints;
}
@end
Make sure to jump over into Interface Builder and hook up the
IBOutlet
to the map view and the delegate of the map view to the view controller, owner
.One item we need to build is a value object to hold the data about our seismic events. This is a pretty normal object, the only thing specific for this is that we are going to implement the
MKAnnotation
protocol. This protocol defines properties for an object that wants to be used as an annotation on a map. There is one property we want to implement which is coordinate
of type CLLocationCoordinate2D
. The header for our object looks like the following.#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
@interface SeismicEvent : NSObject <MKAnnotation>{
float latitude;
float longitude;
float magnitude;
float depth;
}
@property (nonatomic) float latitude;
@property (nonatomic) float longitude;
@property (nonatomic) float magnitude;
@property (nonatomic) float depth;
//MKAnnotation
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@end
#import <MapKit/MapKit.h>
@interface SeismicEvent : NSObject <MKAnnotation>{
float latitude;
float longitude;
float magnitude;
float depth;
}
@property (nonatomic) float latitude;
@property (nonatomic) float longitude;
@property (nonatomic) float magnitude;
@property (nonatomic) float depth;
//MKAnnotation
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@end
The implementation file isn't much more, the only thing we do is make sure to return a
CLLocationCoordinate2D
made up of our latitude and longitude for the getter of coordinate
. I also dropped in the description
method for making debugging easier.#import "SeismicEvent.h"
@implementation SeismicEvent
@synthesize latitude;
@synthesize longitude;
@synthesize magnitude;
@synthesize depth;
@synthesize coordinate;
- (CLLocationCoordinate2D)coordinate
{
CLLocationCoordinate2D coord = {self.latitude, self.longitude};
return coord;
}
- (NSString*) description
{
return [NSString stringWithFormat:@"%1.3f, %1.3f, %1.3f, %1.1f",
self.latitude, self.longitude, self.magnitude, self.depth];
}
@end
@implementation SeismicEvent
@synthesize latitude;
@synthesize longitude;
@synthesize magnitude;
@synthesize depth;
@synthesize coordinate;
- (CLLocationCoordinate2D)coordinate
{
CLLocationCoordinate2D coord = {self.latitude, self.longitude};
return coord;
}
- (NSString*) description
{
return [NSString stringWithFormat:@"%1.3f, %1.3f, %1.3f, %1.1f",
self.latitude, self.longitude, self.magnitude, self.depth];
}
@end
The next task is going out and getting the data from the USGS site. I should mention that I have no idea if you're allowed to use the data I am pulling for external applications or not. I simply found the file link and used it. The data comes in way of a comma separated file from the url http://neic.usgs.gov/neis/gis/qed.asc. We jump into the implementation file for the view controller and work inside
viewDidLoad
for loading the file. Below is the entire method, I will go over the code right after.- (void)viewDidLoad {
[super viewDidLoad];
NSURL *dataUrl = [NSURL
URLWithString:@"http://neic.usgs.gov/neis/gis/qed.asc"];
NSString *fileString = [NSString stringWithContentsOfURL:dataUrl
encoding:NSUTF8StringEncoding
error:nil];
int count = 0;
NSScanner *scanner = [NSScanner scannerWithString:fileString];
eventPoints = [[NSMutableArray array] retain];
SeismicEvent *event;
NSString *line;
NSArray *values;
while ([scanner isAtEnd] == NO) {
[scanner scanUpToString:@"\n" intoString:&line];
//skip the first line
if(count > 0) {
values = [line componentsSeparatedByString:@","];
event = [[[SeismicEvent alloc] init] autorelease];
event.latitude = [[values objectAtIndex:2] floatValue];
event.longitude = [[values objectAtIndex:3] floatValue];
event.magnitude = [[values objectAtIndex:4] floatValue];
event.depth = [[values objectAtIndex:5] floatValue];
[eventPoints addObject:event];
}
count++;
if(count == 300) {
//limit number of events to 300
break;
}
}
[mapView addAnnotations: eventPoints];
}
[super viewDidLoad];
NSURL *dataUrl = [NSURL
URLWithString:@"http://neic.usgs.gov/neis/gis/qed.asc"];
NSString *fileString = [NSString stringWithContentsOfURL:dataUrl
encoding:NSUTF8StringEncoding
error:nil];
int count = 0;
NSScanner *scanner = [NSScanner scannerWithString:fileString];
eventPoints = [[NSMutableArray array] retain];
SeismicEvent *event;
NSString *line;
NSArray *values;
while ([scanner isAtEnd] == NO) {
[scanner scanUpToString:@"\n" intoString:&line];
//skip the first line
if(count > 0) {
values = [line componentsSeparatedByString:@","];
event = [[[SeismicEvent alloc] init] autorelease];
event.latitude = [[values objectAtIndex:2] floatValue];
event.longitude = [[values objectAtIndex:3] floatValue];
event.magnitude = [[values objectAtIndex:4] floatValue];
event.depth = [[values objectAtIndex:5] floatValue];
[eventPoints addObject:event];
}
count++;
if(count == 300) {
//limit number of events to 300
break;
}
}
[mapView addAnnotations: eventPoints];
}
At the top we first build an
NSURL
for the page and then pull in the information into a string. We then need parse the information, this is going to be done with a combination of NSScanner
and separating the string using NSString
. Next up: initializing scanner, events collection, and declaring some variables. Then, we need to loop through the file, using a while
loop checking if the scanner has hit the end of the file. Using scanner to grab the string for an entire line we check to make sure it's not the first line (headers). If it isn't the first line we chop up the string at the commas. With those values we create a new SeismicEvent
and set the appropriate properties on the object, pulling out the correct value for each. The object is then added to the array of events. Still inside the loop we check if we have added 300 and if so break out - we don't want to overcrowd the map. The last thing done in the function is we add the events as annotations on the map.If everything is correct you should get something like the image below, where there are a ton of pins all over the map showing where earthquakes have occurred.
That is pretty cool, especially with the amount of code we have written so far. But what would be cooler? Well, showing the magnitude of the earthquake by changing the pin to a circle that gets larger and more red with increasing magnitude. Ok, so to do this we take advantage of one of the delegate methods on the map view delegate. The method we are looking at is:
- (MKAnnotationView *)mapView:(MKMapView *)lmapView
viewForAnnotation:(id <MKAnnotation>)annotation;
viewForAnnotation:(id <MKAnnotation>)annotation;
The method lets us use a custom view for an annotation. The view that is returned from the method has be a
MKAnnotationView
or a view that extends it. We are going to build a custom view that extends MKAnnotationView
- I named mine EarthquakeEventView
. The only thing in the header for this is an instance variable named event
that is a SeismicEvent
which is going to be set when the annotation is set. The complete header code is below.#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import "SeismicEvent.h"
@interface EarthquakeEventView : MKAnnotationView {
SeismicEvent *event;
}
@end
#import <MapKit/MapKit.h>
#import "SeismicEvent.h"
@interface EarthquakeEventView : MKAnnotationView {
SeismicEvent *event;
}
@end
Jumping over to the implementation file the first thing to do is override the init function,
initWithAnnotation
. In the method I call the super and simply set the background color to clear or transparent - this is important because it allows us to draw semi-transparent graphics and allows the view to have alpha. The next method in our file is going to be to override setAnnotation
. This is where we grab the SeismicEvent
as it comes in and set it to our instance variable. We also set the size of our view as this point to be a height and width of our magnitude squared times 0.75, this makes it a non linear sizing algorithm (tidbit: Richter Scale is logarithmic). We also need to override drawRect
which is where we actually draw our circle. This is done by grabbing the graphics context for the object, setting the color, and drawing the circle. We start with a yellow color and modify it to be more red depending on the magnitude. Finally, it's nice to override dealloc
function to clean up our memory. The entire implementation file follows.#import "EarthquakeEventView.h"
@implementation EarthquakeEventView
- (id)initWithAnnotation:(id <MKAnnotation>)annotation
reuseIdentifier:(NSString *)reuseIdentifier {
if(self = [super initWithAnnotation:annotation
reuseIdentifier:reuseIdentifier]) {
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)setAnnotation:(id <MKAnnotation>)annotation {
super.annotation = annotation;
if([annotation isMemberOfClass:[SeismicEvent class]]) {
event = (SeismicEvent *)annotation;
float magSquared = event.magnitude * event.magnitude;
self.frame = CGRectMake(0, 0, magSquared * .75, magSquared * .75);
} else {
self.frame = CGRectMake(0,0,0,0);
}
}
- (void)drawRect:(CGRect)rect {
float magSquared = event.magnitude * event.magnitude;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBFillColor(context, 1.0, 1.0 - magSquared * 0.015, 0.211, .6);
CGContextFillEllipseInRect(context, rect);
}
- (void)dealloc {
[event release];
[super dealloc];
}
@end
@implementation EarthquakeEventView
- (id)initWithAnnotation:(id <MKAnnotation>)annotation
reuseIdentifier:(NSString *)reuseIdentifier {
if(self = [super initWithAnnotation:annotation
reuseIdentifier:reuseIdentifier]) {
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)setAnnotation:(id <MKAnnotation>)annotation {
super.annotation = annotation;
if([annotation isMemberOfClass:[SeismicEvent class]]) {
event = (SeismicEvent *)annotation;
float magSquared = event.magnitude * event.magnitude;
self.frame = CGRectMake(0, 0, magSquared * .75, magSquared * .75);
} else {
self.frame = CGRectMake(0,0,0,0);
}
}
- (void)drawRect:(CGRect)rect {
float magSquared = event.magnitude * event.magnitude;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBFillColor(context, 1.0, 1.0 - magSquared * 0.015, 0.211, .6);
CGContextFillEllipseInRect(context, rect);
}
- (void)dealloc {
[event release];
[super dealloc];
}
@end
To get this view being used we jump back to our controller implementation file and hook in the delegate function mentioned earlier. So, we can go ahead and add the following to our file.
- (MKAnnotationView *)mapView:(MKMapView *)lmapView
viewForAnnotation:(id <MKAnnotation>)annotation {
EarthquakeEventView *eventView = (EarthquakeEventView *)[lmapView
dequeueReusableAnnotationViewWithIdentifier:
@"eventview"];
if(eventView == nil) {
eventView = [[[EarthquakeEventView alloc] initWithAnnotation:annotation
reuseIdentifier:@"eventview"]
autorelease];
}
eventView.annotation = annotation;
return eventView;
}
viewForAnnotation:(id <MKAnnotation>)annotation {
EarthquakeEventView *eventView = (EarthquakeEventView *)[lmapView
dequeueReusableAnnotationViewWithIdentifier:
@"eventview"];
if(eventView == nil) {
eventView = [[[EarthquakeEventView alloc] initWithAnnotation:annotation
reuseIdentifier:@"eventview"]
autorelease];
}
eventView.annotation = annotation;
return eventView;
}
What is going on above is we use
dequeueReusableAnnotationViewWithIdentifier
to grab an already created view to make reuse of our annotation views. If one isn't returned we create a new one. Then we just set the annotation on it and return the view. Simple as that. Assuming everything is perfect, don't forget to import the correct headers, you should have a fully working application that shows the most recent 300 earthquakes around the world.
No comments:
Post a Comment