Building an Earthquake Monitor for iPhone using MapKit

Skill

Building an Earthquake Monitor for iPhone using MapKit

Posted in:

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.

Get Adobe Flash player

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

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

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

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];
}

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;

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

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

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;
}

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.

As always if you have any questions feel free to leave a comment or head on over to the forums. You can grab the entire project for the demo application below.

Good job!
09/09/2009 - 06:21

I liked your post and how you created a custom annotation views...I'm currently working on MKMapView myself. feel free to join my developer network on Ning: http://codeninja.ning.com/.

reply

pwb
09/17/2009 - 11:25

Have you tested to see if dequeueReusableAnnotationViewWithIdentifier is really finding annotations on the queue to be reused?

reply

The Fattest
09/18/2009 - 22:38

No, not particularly. Is there a reason you would think that they would not be reused?

reply

ed
09/24/2009 - 12:01

hi,

DL your code. Upgraded all to the latest SDK, getting this bug in SesmicEvent.m: (thanks for diving into mapkit)

!synthesize property 'coordinate' must either be named as a
compatiable ivar or must use explicitly name an ivar

reply

Simon
10/13/2009 - 03:20

add CLLocationCoordinate2D coordinate; in SeismicEvent.h with other variable declarations

reply

Simon
10/13/2009 - 03:18

Thanks for an excellent example of practical use!

I am v.new to obj-c ... i notice event release in EarthquakeEventView dealloc ... where is the associated retain ?

reply

Sean
12/11/2009 - 16:04

Thanks for this great example!

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors