Wednesday, December 8, 2010

Building an Earthquake Monitor for iPhone using MapKit


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
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 CLLocationCoordinate2Dmade 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 aMKAnnotationView 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 eventthat 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 drawRectwhich 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(00, 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.01.0 - magSquared * 0.0150.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.

No comments:

Post a Comment

Followers