Chapter 6. Drawing Heat Maps

One of the more obvious missing features of the Map Kit framework is the ability to render heat maps on top of map views. However, while you’ve only met polygon overlays so far (see Chapter 3), it is also possible to use the overlay images on top of maps. You can use that to draw heat maps on top of your maps that will be fixed in the map view and move around as the user scrolls the map back and forth.

Note

The demonstration application we’re going to build in this chapter makes heavy use of heat map code written by George Polak at Skyhook Wireless. Thanks to him and the all the team at Skyhook Wireless, both for the code and assistance. The SHGeoUtils class is copyright Skyhook Wireless and used with permission; it is distributed under an MIT license.

Building an Earthquake Map

You’re going to build a demonstration application with a heat map from data representing the Richter scale magnitude of a recent earthquake in New England.

Open Xcode and choose Create a new Xcode project in the startup window. Choose the Single View Application template from the iOS section of the New Project popup window. When prompted, name your new project HeatMap. In the Company Identifier box, enter the root part of your Bundle Identifier (see the iOS Provisioning Portal) used in your Provisioning Profile; for me, this is uk.co.babilim. You should leave the Class Prefix box blank. Ensure that the Device Family is set to iPhone, the checkbox for ARC is ticked, and the boxes for storyboard and unit tests are not ticked.

Save your project. When the Xcode project window opens, add the Map Kit and Core Location frameworks.

Click the project icon at the top of the Project pane in Xcode, click the main Target for the project, and then click the Build Phases tab. Finally, click the Link Binary with Libraries item to open up the list of linked frameworks, and click the + symbol to add a new framework. Select the MapKit.framework from the drop-down list and click the Add button to add the framework to your project. Then select the CoreLocation.framework from the list and similarly add it to your project.

Now that you’ve imported the Map Kit and Core Location frameworks, click the Supporting Files group in the Project navigator, and then click the Location_Prefix.pch file to open it in the standard editor. Instead of having to include the framework every time you need it, you can use this header file to import it into all the source files in the project, as highlighted in bold below:

#ifdef __OBJC__
    #import <Foundation/Foundation.h>
    #import  <UIKit/UIKit.h>
    #import  <CoreLocation/CoreLocation.h>
    #import  <MapKit/MapKit.h>
#endif

This file is prefixed to all of your source files. However, the compiler precompiles it separately; this means it does not have to reparse the file on each compile run, which can dramatically speed up your compile times on larger projects.

Adding Earthquake Data

To keep things simple, you’re going to provide the earthquake data as a .plist file inside your application bundle and pass that to the code that will generate the overlay image. You can of course use your own data if you like, but the data I’m using here is available for download; if you can’t be bothered, you should just grab that.

Note

You can download the earthquake data I’m using directly from the book’s website.

If you look inside the .plist file, you’ll see something that looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
                       "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <dict>
            <key>magnitude</key>
        <real>3.4</real>
        <key>latitude</key>
        <real>42.7409</real>
        <key>longitude</key>
        <real>-71.4587</real>
    </dict>
    <dict>
        <key>magnitude</key>
        <real>2</real>
        <key>latitude</key>
        <real>41.7387</real>
        <key>longitude</key>
        <real>-72.6697</real>
    </dict>
    <dict>
        <key>magnitude</key>
        <real>2</real>
        <key>latitude</key>
        <real>41.3384</real>
        <key>longitude</key>
        <real>-72.6265</real>
    </dict>
      .
      .
      .
</array>
</plist>

If you haven’t come across one before, a .plist file is often used to store a user’s settings, but it’s also used to store information and configuration data for applications, as you’re doing here.

In any case, drag and drop the data file into your project into the Supporting Files group in the Project Navigator. If you click it afterwards, you should see something like Figure 6-1.

Our earthquake data file inside Xcode

Figure 6-1. Our earthquake data file inside Xcode

Building the User Interface

Now click the ViewController.xib nib file to open the Interface Builder. Open up the Utility panel and drag and drop a Map View (MKMapView) from the Object Library into your view. Then close the Utility panel, open the Assistant editor, and right-click and drag from the map view to your header file to connect the view to your code as an IBOutlet (see Figure 6-2).

Connecting the Map View to the View Controller header file

Figure 6-2. Connecting the Map View to the View Controller header file

Then right-click and drag from the Map View to the App Delegate icon at the top of the dock to make the View Controller a delegate of our new Map View (see Figure 6-3).

Making the View Controller a Map View delegate

Figure 6-3. Making the View Controller a Map View delegate

Once you’ve done that, close the Assistant editor and reopen the Standard editor, and click the ViewController.h interface file. Declare the View Controller as an MKMapViewDelegate as part of the interface declaration:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <MKMapViewDelegate>

@property (weak, nonatomic) IBOutlet MKMapView *mapView;

@end

By clicking the corresponding implementation file, you need to initialize your map view in the viewDidLoad: method:

- (void)viewDidLoad {
    [super viewDidLoad];

    MKCoordinateSpan span = MKCoordinateSpanMake(10.0, 13.0);
    CLLocationCoordinate2D center = CLLocationCoordinate2DMake(39.0, −77.0);
    MKCoordinateRegion region = MKCoordinateRegionMake(center, span);
    self.mapView.region = region;

}

Once you save your changes and click the Run button in the Xcode toolbar, you’ll see that the map will be centered somewhere around Washington D.C. (see Figure 6-4).

Our Map View in the iPhone Simulator

Figure 6-4. Our Map View in the iPhone Simulator

At this point, similar to the polygon-style overlays you saw in Chapter 3, you need to create an object that implements the MKOverlay protocol and a corresponding MKOverlayView object. However, since your image overlays are going to be slightly more involved than the polygons you used last time, you’re going to have to roll your own implementations of these classes.

Adding the Overlay

Right-click on the HeatMap group in the Project Navigator, select New file from the menu, choose an Objective-C class, and make the new class a subclass of NSObject and class it MapOverlay when prompted.

Do this again, but this time you should create your new class as a subclass of the MKOverlayView class rather than NSObject. Name this class MapOverlayView when prompted to do so.

You should end up with four new files; MapOverlay.h, MapOverlay.m, MapOverlayView.h, and MapOverlayView.m.

Click the MapOverlay.h interface file to open it in the Standard editor, declare that it implements the MKOverlay protocol, and change it as follows:

#import <Foundation/Foundation.h>

@interface MapOverlay : NSObject <MKOverlay> {
    CLLocationCoordinate2D _coordinate;
    MKMapRect _mapRect;
    MKMapView *_mapView;
}

- (id)initWithView:(MKMapView *)mapView;
- (MKMapRect)boundingMapRect;

@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, readonly) MKMapRect mapRect;
@property (nonatomic, strong) MKMapView *mapView;

@end

Change it in the corresponding MapOverlay.m implementation file as well:

#import "MapOverlay.h"

@implementation MapOverlay

@synthesize mapView=_mapView;

- (id)initWithView:(MKMapView *)mapView {
    if (self = [super init]) {
        self.mapView = mapView;
        _coordinate = mapView.centerCoordinate;
        _mapRect = mapView.visibleMapRect;
    }
    return self;
}

-(CLLocationCoordinate2D)coordinate {
    return _coordinate;
}

- (MKMapRect)boundingMapRect {
    return _mapRect;
}

@end

You can see that you’re going to pass the Map View you want to put the overlay on top of to your overlay class, and use that map view to configure the center and boundaries of your overlay.

Warning

You should notice that you’re extracting the center and boundaries of the Map View when you initially create the overlay. If you do not do this, then the resulting overlay will not scale correctly as the map view is zoomed in and out.

Essentially you’re going to overlap an image over your entire Map View, or at least the part that is initially visible in the Window when your application starts up.

You can leave the MapOverlayView.h interface file as it is—you don’t need to make any changes there. Instead, open the MapOverlayView.m implementation file and add the required drawMapRect:zoomScale:inContext: method.

#import "MapOverlayView.h"
#import "MapOverlay.h"

@implementation MapOverlayView

- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale
                                       inContext:(CGContextRef)ctx {

    // Load in earthquake data
    NSString *dataFile = [[NSBundle mainBundle] pathForResource:@"EarthQuake-Data"
                                                         ofType:@"plist"];
    NSArray *quakeData = [[NSArray alloc] initWithContentsOfFile:dataFile];

    NSMutableArray *locations =
        [[NSMutableArray alloc] initWithCapacity:[quakeData count]];
    NSMutableArray *weights =
        [[NSMutableArray alloc] initWithCapacity:[quakeData count]];

    for (NSDictionary *reading in quakeData) {
        CLLocationDegrees latitude =
            [[reading objectForKey:@"latitude"] doubleValue];
        CLLocationDegrees longitude =
            [[reading objectForKey:@"longitude"] doubleValue];
        double magnitude = [[reading objectForKey:@"magnitude"] doubleValue];

        CLLocation *location =
            [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
        [locations addObject:location];
        [weights addObject:[NSNumber numberWithInteger:(magnitude * 10)]];
    }

    //Loading and set up the image overlay
    MKMapRect theMapRect = [self.overlay boundingMapRect];
    CGRect theCGRect = [self rectForMapRect:theMapRect];

    MapOverlay *overlay = (MapOverlay *)self.overlay;
    MKMapView *view = overlay.mapView;

    UIImage *heatmap = nil; // ... insert code here to generate the heatmap ...
    CGImageRef imageRef = heatmap.CGImage;

    // Flip and reposition the image
    CGContextScaleCTM(ctx, 1.0, −1.0);
    CGContextTranslateCTM(ctx, 0.0, -theCGRect.size.height);

    //drawing the image to the context
    CGContextDrawImage(ctx, theCGRect, imageRef);
}

@end

Go back to the ViewController.m implementation file and import both your new classes:

#import "MapOverlay.h"
#import "MapOverlayView.h"

Then do the same in the viewDidLoad: method:

- (void)viewDidLoad {
    [super viewDidLoad];

    MKCoordinateSpan span = MKCoordinateSpanMake(10.0, 13.0);
    CLLocationCoordinate2D center = CLLocationCoordinate2DMake(39.0, 77.0);
    MKCoordinateRegion region = MKCoordinateRegionMake(center, span);
    self.mapView.region = region;

    MapOverlay * mapOverlay = [[MapOverlay alloc] initWithView:self.mapView];
    [self.mapView addOverlay:mapOverlay];

}

Create your overlay. Finally, you need to add the delegate callback to the same class:

- (MKOverlayView *)mapView:(MKMapView *)mapView
            viewForOverlay:(id <MKOverlay>)overlay {

    MapOverlay *mapOverlay = (MapOverlay *)overlay;
    MapOverlayView *mapOverlayView =
        [[MapOverlayView alloc] initWithOverlay:mapOverlay];

    return mapOverlayView;
}

If you save your changes and click the Run button in the Xcode toolbar, your application should successfully build and deploy into the iPhone Simulator. Of course, this happens right at the moment the overlaid image is set to nil, so the application will look exactly the same as it did in Figure 6-4, but you should be able to check that everything builds and runs correctly.

Adding the Heat Map

Let’s fix that right now. To do that, you’re going to make use of the SHGeoUtils class, written by George Polak of Skyhook Wireless.

Note

You can download the SHGeoUtils class files from the book’s website at:

http://programmingiphonesensors.com/code/SHGeoUtils.h

http://programmingiphonesensors.com/code/SHGeoUtils.m

The code is released under an MIT license.

Looking at the SHGeoUtils class, you can see that it provides three class methods. All of them will return a UIImage that can then be used by your MapOverlayView class.

#import <Foundation/Foundation.h>

@interface SHGeoUtils : NSObject

+ (UIImage *)heatMapForMapView:(MKMapView *)mapView
        boost:(float)boost
        locations:(NSArray *)locations
        weights:(NSArray *)weights;1

+ (UIImage *)heatMapWithRect:(CGRect)rect
        boost:(float)boost
        points:(NSArray *)points
        weights:(NSArray *)weights;2

+ (UIImage *)heatMapWithRect:(CGRect)rect
        boost:(float)boost
        points:(NSArray *)points
        weights:(NSArray *)weights
        weightsAdjustmentEnabled:(BOOL)weightsAdjustmentEnabled
        groupingEnabled:(BOOL)groupingEnabled;3

@end
1

This generates a heat map image for the specified map view. You pass the locations as an NSArray of CLLocation objects and the weights as an NSArray of NSNumber objects corresponding to the weighting of each point. There should be a one-to-one correspondence between the location and weight elements. Passing a nil weight parameter implies an even weight distribution between the location points. The boost value represents a radius multiplier: values close to 1.0 will produce very diffuse maps, while values close to 0.0 will produce maps that are pointlike in nature.

2

Instead of specifiying a map view, you are asked to specifiy a CGRect region frame for the view and your locations as an NSArray of NSValue CGPoint objects representing the data points.

3

The final method allows you to adjust the weighting of the points you pass to the heat map code: setting YES allows weight balancing and normalization to occur. You can also enable grouping: setting YES allows for tighter visual grouping of dense areas of data.

Download the utility classes and drag and drop them into your project.

You’ve set up your overlay view so that you can easily use the first—and most convenient—of the three methods provided by the SHGeoUtils class. Click the MapOverlayView.m implementation file to open it in the editor, and in the drawMapRect:zoomScale:inContext: method, make the following change:

- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale 
                                       inContext:(CGContextRef)ctx {

    // Load in earthquake data
    NSString *dataFile =
        [[NSBundle mainBundle] pathForResource:@"EarthQuake-Data" ofType:@"plist"];
    NSArray *quakeData = [[NSArray alloc] initWithContentsOfFile:dataFile];

    NSMutableArray *locations =
        [[NSMutableArray alloc] initWithCapacity:[quakeData count]];
    NSMutableArray *weights =
        [[NSMutableArray alloc] initWithCapacity:[quakeData count]];

    for (NSDictionary *reading in quakeData) {
        CLLocationDegrees latitude =
            [[reading objectForKey:@"latitude"] doubleValue];
        CLLocationDegrees longitude =
            [[reading objectForKey:@"longitude"] doubleValue];
        double magnitude =
            [[reading objectForKey:@"magnitude"] doubleValue];

        CLLocation *location =
            [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
        [locations addObject:location];
        [weights addObject:[NSNumber numberWithInteger:(magnitude * 10)]];
    }

    //Loading and set up the image overlay
    MKMapRect theMapRect = [self.overlay boundingMapRect];
    CGRect theCGRect = [self rectForMapRect:theMapRect];

    MapOverlay *overlay = (MapOverlay *)self.overlay;
    MKMapView *view = overlay.mapView;

    UIImage *heatmap = [SHGeoUtils heatMapForMapView:view
                                               boost:0.67
                                           locations:locations
                                             weights:weights];
    CGImageRef imageRef = heatmap.CGImage;

    // Flip and reposition the image
    CGContextScaleCTM(ctx, 1.0, 1.0);
    CGContextTranslateCTM(ctx, 0.0, -theCGRect.size.height);

    //drawing the image to the context
    CGContextDrawImage(ctx, theCGRect, imageRef);
}

Save your changes and click the Run button in the Xcode editor. If all goes well, you should see something like Figure 6-5.

Your map with a heat map of the earthquake data overlaid on top (center), zoomed out (left), and zoomed in (right)

Figure 6-5. Your map with a heat map of the earthquake data overlaid on top (center), zoomed out (left), and zoomed in (right)

At this point, you should be able to pan the map as normal, rotate the interface into landscape mode and back, and pinch and zoom. The heat map should move and scale, just as you’d expect.

Overlaying Other Data

At this point, you can generalize your MapOverlay and MapOverlayView classes somewhat to cope with arbitrary data. For instance, you can modify your MapOverlay class to take both the locations and weights during initialization rather than reading these in inside your MapOverlayView class. Changing the MapOverlay.h interface file as below:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MapOverlay : NSObject <MKOverlay> {
    CLLocationCoordinate2D _coordinate;
    MKMapRect _mapRect;
    MKMapView *_mapView;
    NSArray *_locations;
    NSArray *_weights;
}

- (MKMapRect)boundingMapRect;

- (id)initWithView:(MKMapView *)mapView
      andLocations:(NSArray *)locations
        andWeights:(NSArray *)weights;


@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, readonly) MKMapRect mapRect;
@property (nonatomic, strong) MKMapView *mapView;
@property (nonatomic, strong) NSArray *locations;
@property (nonatomic, strong) NSArray *weights;

@end

along with the MapOverlay.m implementation

#import "MapOverlay.h"

@implementation MapOverlay

@synthesize mapView=_mapView;
@synthesize locations=_locations;
@synthesize weights=_weights;

- (id)initWithView:(MKMapView *)mapView
      andLocations:(NSArray *)locations
        andWeights:(NSArray *)weights {
    if (self = [super init]) {
        self.mapView = mapView;
        self.locations = locations;
        self.weights = weights;
        _coordinate = mapView.centerCoordinate;
        _mapRect = mapView.visibleMapRect;

    }
    return self;
}

-(CLLocationCoordinate2D)coordinate {
    return _coordinate;
}

- (MKMapRect)boundingMapRect {
    return _mapRect;
}

@end

allows us to modify the MapOverlayView.m implementation.

#import "MapOverlayView.h"
#import "MapOverlay.h"
#import "SHGeoUtils.h"

@implementation MapOverlayView

- (void)drawMapRect:(MKMapRect)mapRect
          zoomScale:(MKZoomScale)zoomScale
          inContext:(CGContextRef)ctx {

    //Loading and setting the image
    MKMapRect theMapRect = [self.overlay boundingMapRect];
    CGRect theCGRect = [self rectForMapRect:theMapRect];

    MapOverlay *overlay = (MapOverlay *)self.overlay;
    MKMapView *view = overlay.mapView;

    UIImage *heatmap = [SHGeoUtils heatMapForMapView:view
                                               boost:0.67
                                           locations:overlay.locations
                                             weights:overlay.weights];
    CGImageRef imageRef = heatmap.CGImage;

    // We need to flip and reposition the image here
    CGContextScaleCTM(ctx, 1.0, 1.0);
    CGContextTranslateCTM(ctx, 0, -theCGRect.size.height);

    //drawing the image to the context
    CGContextDrawImage(ctx, theCGRect, imageRef);
}

@end

Overlaying Other Types of Images

You might have noticed that your call to the heat map code hooks into your overlay code in just one place, so your code can be easily modified to display any image. If you drag and drop an image of an American flag into your project and edit the drawMapRect:zoomScale:inContext: method in the MapOverlayView implementation to pull that image instead of your earthquake data heat map, you get something like Figure 6-6 when you build and run the new code.

A zoomed-out view of the image overlay.

Figure 6-6. A zoomed-out view of the image overlay.

For example:

- (void)drawMapRect:(MKMapRect)mapRect
          zoomScale:(MKZoomScale)zoomScale
          inContext:(CGContextRef)ctx {

    //Loading and setup the image
    MKMapRect theMapRect = [self.overlay boundingMapRect];
    CGRect theCGRect = [self rectForMapRect:theMapRect];

    UIImage *image  = [UIImage imageNamed:@"Flag.png"];
    CGImageRef imageRef = heatmap.CGImage;

    // We need to flip and reposition the image here
    CGContextScaleCTM(ctx, 1.0, 1.0);
    CGContextTranslateCTM(ctx, 0.0, -theCGRect.size.height);

    //drawing the image to the context
    CGContextDrawImage(ctx, theCGRect, imageRef);
}

The size of the flag denotes the size of the extent of the original map view as your current overlay code scales the size of the overlaid object to be the size of the existing map view on startup.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset