Tag Archives: MKMapView

OpenGL on top of Mapview part 1

It’s been a while since I wrote the second post about 3D on map. I have had to juggle few balls constantly up on air, so please bear with me.

Before moving on, I’d like to point out that there are *MANY* ways to achieve what I am trying here; “draw interesting places/events in 3D on map”. However the case might be, you might have to consider one very important factor, MONEY.

For example, you can buy and store full pre-rendered World map into iPhone. Then load piece by piece like 2D texture into OpenGL space to render from map to stars. Hmmm… I don’t think that I can even buy the map myself.

Another one is to walk straight up to Eric Schmidt and strike a deal for Google 3D map service. If I could do this, I shouldn’t even write these posts.

You can go with full set of map library like Route-me and map tile services like Cloudmade. Route-me is free and Cloudmade is free for small amount of load. Unfortunately even this is rather costly approach for low-profile start-ups like me. Just take rough estimate of workload and monthly service charge when your project goes over the “free” limit. Hmmm…

This post is all about “Make it pretty and make it cheap”. What’s left is then to come up with a way to combine OpenGL and iPhone mapview (MKMapView class). MkMapView is Cocoa class that provides Google map with smooth scroll and zoom. Besides, it calculates latitude and longitude of screen position on Mercator projection and vice versa. As with other native Cocoa classes, subclassing and categorizing it are fine so you have very powerful resource at your disposal already. Let’s get started from the two. By the way, if you have any better idea, let me know. I crave for it all the times.

*A map tile is small piece of Mercator projection of World map at a latitude and longitude. The piece-wise fragmentation of map makes loading World map into lightweight environments like web browsers and iPhone possible and feasible. Meanwhile, you’d need a rather non-trivial amount calculation to place map tiles on correct locations of screen. In addition, the size of entire map tiles for World is humongous. This is why you need map library and map tile service. Offline map tile storage is possible but just take a look at the size of Tom-tom GPS. Don’t forget it does not cover the entire World at various scales.

**Currently Google does not provide map tile service. Microsoft Bing and Yahoo! Map provide it though. You might have to pay handsomely beforehand. Cloudmade isn’t completely free. Nonetheless it is a newcomer compared to three former and price is rather reasonable. Plus, Cloudmade staffs talk with their customers.)

OpenGLES version and EAGLView

Ok, let me pick up from what was left off last time. 3D rendering on iPhone requires to use an OpenGLES (OpenGL for Embedded Systems, A good place for memory refresher is Jeff  Lamarche’s OpenGL ES from the Ground Up.) and a special view, namely EAGLView.

For the version of OpenGLES to use, (Of course, there are versions! Even bible has versions of new and old!) you can choose 1.1 to cover every iPhone/iPod Touch on earth, or 2.0 to cover from iPhone 3GS /iPod Touch 3rd gen. onward. Without a doubt, OpenGLES 2.0 is much more pretty and powerful in displaying 3D objects.

Since I want to reach as broad audiences as there possibly are, my choice is definitely to go with OpenGLES 1.1, and all my examples here are written with OpenGLES 1.1 in mind. If you want to apply them to OpenGLES 2.0, you might have different outcomes than what I have.

Cocoa does not provide native EAGLView class. EAGLView is what Xcode hands you from template when you choose to make an “OpenGL ES Application”. If you take a look inside, you’ll notice it is just a plain UIView subclass with its CALayer configured for custom OpenGL rendering.

EAGLView and MKMapView come together

The first thing you need to do is to stack two views: EAGLView (OpenGLES), and MKMapview (Mapview).

Stacking EAGLView and MKMapView is as simple as adding UIViews into UIViewController. Just add MKMapView first and EAGLView later.


// MKMapView initializing...
MKCoordinateRegion region;
region.center.latitude = 37.712615;
region.center.longitude = -122.472839;
region.span.longitudeDelta = 14.0625;
[(MKMapView *)self.view setRegion:region animated:NO];

//EAGLView (in OpenGLES 1.x) initializing...
EAGLContext *aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
self.context = aContext;
[aContext release];
[oglesView setContext:context];
[oglesView setFramebuffer];
[self.view addSubview:oglesView];

Once you stack them up, you might want to run Timer or CADisplayLink to render 60 frames for every second (60 fps). (There might be other ways to animate 3D objects such as using event trigger. But I have not tested yet nor confronted solid examples so I cannot tell you drop timer off.) Since iPhone hardware screen refresh rate is locked into 60 fps, you don’t need go faster than that.

Transparent EAGLView background

Once you start render and draw, what you’ll see is an oscillating colorful rectangle with gray background. In order to see the MKMapView in the back, you need to make EAGLView transparent. You need to touch two places. (Transparent EAGLView does not require OpenGL alpha blending. Unless you really need to use, you don’t have to turn on blending feature.)

// EAGLView.m
//-(id)initWithCoder:(NSCoder*)coder

//UIView background transparency
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];

//Then CAEAGLLayer background transparency
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = NO;

//work around for "unavailable error"
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
const CGFloat myColor[] = {0.0, 0.0, 0.0, 0.0};
eaglLayer.backgroundColor = CGColorCreate(rgb, myColor);
CGColorSpaceRelease(rgb);

Then when you draw a frame, you clear the whole screen with alpha value of 0.0. As long as your alpha is 0.0, you can use any color.

//mapviewViewController.m
//-(void)drawFrame
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

Compile it and run, and then things look just fine. The mapview will display bay area of San Francisco, and 3D objects (in this case just a 2D rectangle) will be displayed on top of the map. What a breeze!

MkMapView and Re-routing UIEvent

When you start scroll the mapview, you will notice the mapview doesn’t really budge. You can zoom in and out but that is it.

…..ok. What’s going on?

What’s really peculiar about MKMapView class is it does not want anything to sit on top of it. If a UIView does so, MKMapView will not accept UIEvent. It has behaved this way ever since.

To get around this issue,

1) we need to firstly set EAGLView’s “userInteractionEnabled” to “NO”. Yes, that is right. This will completely block every UIEvent coming through.

2) Then we’ll have to make a UIWindow subclass to re-route touch event to MKMapView.

3) Just in case you still want to receive user event, I put a hole in EAGLView with “-(void) handleTouches:(UIEvent*)event”.

Look at the code below.

// EAGLView.m
-(id)initWithCoder:(NSCoder*)coder
// U don't want this view to listen user touch event
self.userInteractionEnabled = NO;
}

- (void) handleTouches:(UIEvent*)event {
}

UIWindow subclass and how it is implemented in app delegate.

@implementation EventRouteWindow
@synthesize overlayListener;
- (void)sendEvent:(UIEvent*)event {
	[super sendEvent:event];
	if(overlayListener != nil && [overlayListener respondsToSelector:@selector(handleTouches:)])
		[overlayListener performSelector:@selector(handleTouches:) withObject:event];

}
@end

@implementation AppDelegate
-(void)applicationDidFinishLaunching:(NSNotification*)aNotification{
     [self.window addSubview:viewController.view];
     [(EventRouteWindow*)window setOverlayListener:[viewController oglesView]];
     [self.window makeKeyAndVisible];
}
@end

As we get in “-(void)sendEvent:(UIEvent*)event” of UIWindow class, we are touching the lowest possible level of UIEvent delivery chain. With the UIEvent object, you can do just about anything. In fact, touches* function family of UIView handle just derivatives of UIEvent object delivered from here.

If we’re to draw an overview of how this event routing works, it should look like following.

Isn’t this dirty hack of some sort, you just ask? Yes, kind of. ;) However, this is not part of private framework that might put you in a trouble later.

Are there other options to achieve this workaround? Of course, there are many, if not thousands. I just don’t like objects at the same logical level exchanging pointers from one to another.

Ok, let’s compile and run again, and now map will scroll and zoom. You will quickly notice OpenGL will stop rendering whenever you touch the screen.  Just gentle touch will put your renderer in halt. Even if there is no link whatsoever to connect the two views, your renderer will stop until you pull of your finger from screen.

In fact, this is why I started blogging about it. There still remain a few issues to work around. My good friend, I’d like to take a journey with you on conquering those monsters. I’ll cover why this is happening, and how we can get around it next time.

If you have any question, find a mistake, and/or just want to say hello, drop me a line at twitter.com/stkim1. Download and enjoy the example!