Size an MKMapView to Fit its Annotations in iOS Without Futzing with Coordinate Systems

map viewAn oddity of MKMapView is that it always defaults to showing the western hemisphere of the Earth. That’s almost certainly not what you want. Instead, you have probably set an array of id <MKAnnotation> objects which you want to appear as pins on the map. If the pins were evenly distributed over the USA, then great. Otherwise, the MapView is not at all zoomed the way you want. It baffles me a map view doesn’t just zoom itself by default when you set annotations, but it doesn’t.

One approach might be to loop over all of your annotations and figure out what the max and min x and y values are across them all. Then get the center point of the map view and do some math to convert the coordinates and scale the map view.

Alternatively, MapKit has API that is separate from the map view itself for manipulating rectangles and regions which can do the job automatically.

The main problem is that id <MKAnnotation> objects contain a CoreLocation coordinate – CLLocationCoordinate2D – which represents a GPS coordinate while MapView has an MKCoordinateRegion which has a CLLocationCoordinate2D defining its center but uses an MKCoordinateRegion span struct to define the zoom level of the map view in terms of degrees of arc.

MapKit provides a mix of object-oriented API and C functions to convert an array of CLLocationCoordinate2D structs from id <MKAnnotation> objects into an MKCoordinateRegion suitable for setting the region in a MKMapView. These transformations are straightforward and don’t require fiddling with converting between coordinate systems.

Here is the code in a map view controller

#define MINIMUM_ZOOM_ARC 0.014 //approximately 1 miles (1 degree of arc ~= 69 miles)
#define ANNOTATION_REGION_PAD_FACTOR 1.15
#define MAX_DEGREES_ARC 360
//size the mapView region to fit its annotations
- (void)zoomMapViewToFitAnnotations:(MKMapView *)mapView animated:(BOOL)animated
{ 
    NSArray *annotations = mapView.annotations;
    int count = [mapView.annotations count];
    if ( count == 0) { return; } //bail if no annotations
    
    //convert NSArray of id <MKAnnotation> into an MKCoordinateRegion that can be used to set the map size
    //can't use NSArray with MKMapPoint because MKMapPoint is not an id
    MKMapPoint points[count]; //C array of MKMapPoint struct
    for( int i=0; i<count; i++ ) //load points C array by converting coordinates to points
    {
        CLLocationCoordinate2D coordinate = [(id <MKAnnotation>)[annotations objectAtIndex:i] coordinate];
        points[i] = MKMapPointForCoordinate(coordinate);
    }
    //create MKMapRect from array of MKMapPoint
    MKMapRect mapRect = [[MKPolygon polygonWithPoints:points count:count] boundingMapRect];
    //convert MKCoordinateRegion from MKMapRect
    MKCoordinateRegion region = MKCoordinateRegionForMapRect(mapRect);
    
    //add padding so pins aren't scrunched on the edges
    region.span.latitudeDelta  *= ANNOTATION_REGION_PAD_FACTOR;
    region.span.longitudeDelta *= ANNOTATION_REGION_PAD_FACTOR;
    //but padding can't be bigger than the world
    if( region.span.latitudeDelta > MAX_DEGREES_ARC ) { region.span.latitudeDelta  = MAX_DEGREES_ARC; }
    if( region.span.longitudeDelta > MAX_DEGREES_ARC ){ region.span.longitudeDelta = MAX_DEGREES_ARC; }
    
    //and don't zoom in stupid-close on small samples
    if( region.span.latitudeDelta  < MINIMUM_ZOOM_ARC ) { region.span.latitudeDelta  = MINIMUM_ZOOM_ARC; }
    if( region.span.longitudeDelta < MINIMUM_ZOOM_ARC ) { region.span.longitudeDelta = MINIMUM_ZOOM_ARC; }
    //and if there is a sample of 1 we want the max zoom-in instead of max zoom-out
    if( count == 1 )
    { 
        region.span.latitudeDelta = MINIMUM_ZOOM_ARC;
        region.span.longitudeDelta = MINIMUM_ZOOM_ARC;
    }
    [mapView setRegion:region animated:animated];
}

- (void)viewWillAppear:(BOOL)animated
{
    [self zoomMapViewToFitAnnotations:self.mapView animated:animated];
    //or maybe you would do the call above in the code path that sets the annotations array
}
Advertisement

Nexus One Came Back to Life After a Soaking

I’ve had my Nexus One in a bag of rice for the last 5 days to draw the water out of it. I put the battery back in this morning and it booted up with a working touch screen and no evident ill effects except that it has decided to erase all my personal data and apps.

All of the data on it is synced onto cloud services and onto my iPhone and desktop so nothing is lost. It is a big hassle to get apps back on the Nexus One though because the Android Market store has some really unfortunate carrier blocking. Some apps such as GMail, Google Listen, Google Voice and Amazon Kindle do not appear in the store unless the carrier identified by your SIM is white-listed with Google. This is one thing that Apple seems to get really right. The iOS App Store seems to work regardless of locality or carrier.

I’ve decided to stick with iPhone for a while. iOS 4 is a significant improvement. It seems to achieve most of the key benefits that Android has while retaining a slicker GUI and avoiding the glitchy touch-screen registration issues and periodic hangs that I experience regularly with Android 2.2 on the Nexus One.

Drowned Nexus One

Last Thursday, I was caught out in a serious deluge of a storm and was thoroughly soaked to the skin. When I got home, I pulled my Nexus One out of my pocket and watched in horror as the screen flickered and died. I have the Nexus One in a bag of rice hoping it will dry out and revive but I can say this is one area where the iPhone design is better. My iPhone 3Gs was more water resistant. I was drenched in a similar way in a rain storm with my iPhone in my pocket and it survived without any water damage. I chalk this up to the don’t-open-me sealed design of the iPhone case vs the pop-open Nexus One with a removable battery. I do seem to manage to destroy phones at about the one-year mark with alarming predictability, though, regardless of manufacturer.

Time to get another phone and quickly. I’m willing to pay for an unlocked device rather than wasting my time futzing around rooting devices. That’s why I got the Nexus One in the first place. On the other hand, I’m really, really irritated that Gingerbread is still not out on the Nexus One. And the price at the Telefonika in the Accra Mall for a new Nexus One is roughly $900! At the current exchange rates, the price for a new unlocked iPhone in Ghana is nearly $1500! RIM BlackBerry phones carry a similar premium and are immensely popular here.

photo

As I’m unwilling to pay $1500 for a phone if I can avoid it, I find myself having to pull my pre-Nexus One iPhone 3Gs out of a shipping crate so that I can get it working here in Ghana. My wife has an AT&T BlackBerry which AT&T unlocked without comment but AT&T will not unlock an iPhone for any reason. (FWIW, I am still an AT&T customer. My company pays for a full monthly data plan with international roaming but I normally use Airtel in Ghana.)

These are not reasonable prices in my world. I have an iPhone locked to AT&T and running iPhone OS 3.2. What I wanted was iOS 4.2.1 but I wasn’t going to shell out $1500 to get it. So, despite my desire to be a good little consumer and my willingness to pay something of a premium to get an unlocked phone, I found myself researching how to jailbreak my iPhone 3Gs. It took me some time that I’ll never get back but this is what I came up with:

  1. I used Pwnage 4.1 to build a custom 4.2.1 iOS image without the baseband update from iOS 4.2.1. To do this you need the iOS image and you have to add a iPhone 3Gs 4.2.1 bundle to Pwnage. This is important because the baseband I had is unlockable using UltraSn0w but the baseband from iOS 4.2.1 is not and the solution to that problem is flashing a baseband firmware from the iPad which makes it impossible to go back to a stock iOS image.
  2. I kept getting an error from iTunes when I tried to flash the firmware onto my phone. It turns out that I needed to have iTunes 10.1 and I had whatever comes with Snow Leopard because I don’t actually use iTunes. But after doing the update to the latest iTunes 10.1.x, it worked.
  3. Then I used greenpois0n to jailbreak the phone because it seems to be the only jailbreak for iOS 4.2.1 that works without having to tether the phone to a computer for every reboot..This worked on the second try.
  4. Basically, what I had at this point was a stock 4.2.1 image with an older baseband and an extra green icon called “Loader”. What Loader does is install Cydia on the root partition and it failed because the partition was too small. Back to step 1, this time I created an image with a 1024MB root partition.
  5. Once the phone was jailbroken with Cydia on it, I was able to use Cydia to install UltraSn0w which unlocks the baseband for any SIM. Finally my phone was up and running on Airtel.

I’m not very happy about this state of affairs but there it is. I am happy to have a working phone again.

Goodbye iPhone 3Gs, Hello Nexus One

nexus-one-logo

I have really enjoyed my iPhone and mobile Safari but AT&T and Apple will not unlock an iPhone for any reason. I am moving to Ghana and there is no way that I will be roaming with an AT&T international plan. It seems that iPhones are not yet sold in Ghana so I would have to try to buy an unlocked one in Amsterdam in transit or buy something in Ghana.

I couldn’t resist the new Google Nexus One. It was almost tailor-made for solving my problem. MTN in Ghana has 900MHz UMTS, which is supported by the Nexus One. And, of course, the Nexus One is sold unlocked.

Initial key selling points:

  • Unlocked
  • Can be tethered (via rooting or PDANet)
  • Integration with Google Apps
  • Multitasking
  • Not tied to iTunes or any other desktop software

Android is a Java-like VM called Dalvik which runs on top of a core Linux OS and some C libraries like WebKit and SQLite. I was definitely concerned that the phone would be sluggish or klunky. It is neither. It is fast and it is slick.

My initial impression is that this phone has everything that I loved about the iPhone and either has built-in or add-on fixes for all the things that annoyed me.

device

Initial observations:

  • Phone “activation” is dead simple. You sign in with a Google Apps, regular Google or Exchange account (or a combo) and you’re done.
  • This thing seems faster than the iPhone 3Gs.
  • WiFi performance is great and Edge (I’m still on AT&T) seems the same as the “3G” (which often falls back to Edge) performance I have been getting in my little slice of Washington, DC.
  • The Voice app is like Visual Voicemail on steroids. Yeah, sometimes the transcripts are bad but then you fall back to the same behavior as Visual Voicemail. Incidentally, you don’t need a Google number for Voice to work. It sets itself up with your carrier to replace the voicemail system provided by your carrier and you can turn it off if you want to go back.
  • Gadgets are a cool way to embed applications like calendar, weather and news and search right in your home screens.
  • The Google Market store for apps is well done and it’s a nice touch that there is an Amazon music store app.
  • Listen is fantastic at pulling down podcasts over the air and caching them. It also can pull them down in real time and I can still surf the web at the same time. On iPhone this wasn’t possible because the media streaming component of Safari is modal. You can’t do anything else with Safari unless you kill the stream.
  • I love that the alarm clock has a cock crowing ringtone.

I have been seeing more and more Droid phones popping up around town. This feels like the future.

C# as Universal Smart Phone Programming Language?

We started thinking about building a smart phone app to interface to PeopleMatrix. The obvious devices to support are BlackBerry, iPhone, Android and Windows Mobile. There is also Symbian but those devices are unusual in our primary market. Each one of these platforms has a totally different programming model:

  • BlackBerry –> Java ME + RIM libraries
  • iPhone –> Objective-C
  • Android –> Subset of Java 5 + Apache commons and  Android libraries
  • Windows Mobile –> C/C++ and .NET CF
  • Symbian –> Weird non-standard Symbian C++ variant and Qt

I just can’t envision anyone using their smartphone to interact with a sophisticated app on a screen the size or a postage stamp. That eliminates Blackberry and (many) Windows Mobile devices. Also, you have to prioritize developing for the device platforms that are growing. That means iPhone and Andoid. iPhone is very popular and Android has shown amazing growth.

The problem is that the development environment is totally different so that porting applications between Android and iPhone is a complete re-write. One ray of hope for leveraging code across these platforms is the Mono project. Novell is currently shipping a product called MonoTouch which compiles C# code into native binaries for the iPhone. The Mono guys also have Mono working on Android with proxy classes that call into the Android libraries. (In early testing Mono appears to out-perform Dalvik, too.)

If Mono on Android gets polished up like MonoTouch, that would make C# a first class programming language for a huge swath of the most exciting devices. The largest challenge for managing the codebase of an app is that it is very likely that each platform would require care to abstract access to platform-native APIs which would certainly include the GUI and other hardware interfaces.

Even so, I am watching Mono closely. Interesting times.

%d bloggers like this: