Usual way to display maps on iOS devices is using Map Kit framework that enables you to embed maps directly into your application and adds support for annotating those maps. Problem arises when you need to display large number of pins or annotations. Device performance degrades as number of pins increases and usability of UI suffers greatly. Recently we were required to develop an application that includes map view with almost 1000 places of interest on relatively small geographical area. To solve those mentioned problems we had to find annotations that are close one to another and reduce their number.

First to even show a map view we need to add the MapKit and CoreLocation frameworks to our project – in Xcode 4 Project navigator select project icon, select target that we are building, in Build Phases tab select Link With Binary section, click on a little + sign and add MapKit.framework and CoreLocation.framework.

Next we need to add MKMapView object to controller that controls view where we want the map to be displayed and setting  MKMapView delegate to the same controller. In code example provided on the end of the tutorial that controller is called mapViewController and its header file looks like this:

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import <MapKit/MKAnnotation.h>
#import "MyPlace.h"
@interface mapViewViewController : UIViewController <MKMapViewDelegate> {
    IBOutlet MKMapView *myMapView;
    NSArray *places;
    CLLocationDegrees zoomLevel;
}
-(void)loadDummyPlaces;
-(float)RandomFloatStart:(float)a end:(float)b;
-(void)filterAnnotations:(NSArray *)placesToFilter;
@end

mapViewController class conforms to MKMapViewDelegate protocol and that enables us to display MKMapView object inside corresponding view. Array places is an array of MyPlace objects that we will use to populate map with annotations. MyPlaces is a class that conforms to a MKAnnotation protocol and contains information as title, subtitle and geographical location of object that we want to show on our map.  In our example method loadDummyPlaces populates same array with 1000 random places, MyPlace objects.

After execution of  [myMapView addAnnotations:places] method which adds all objects in places array to our map view our iOS device screen looks like this:

A bit crowded isn’t it?

So what we have done? We decided to filter those places and show only few of them, and as user zooms in show him some more details… First we place one annotation on map view, then when adding a new one we check if new one is on the same area as the first one, and if it is not we add it to our map view, if it is not we check it agains next annotation placed on map.

Geographical areas that annotations cover change as map zoom level is changed so we need to calculate them for each zoom level. for the simplicity of this example we are using two defined constants iphoneScaleFactorLatitude and iPhoneScaleFactorLongitude that are calculated using simple formulas:
iphoneScaleFactorLatitude = deviceScreenWidth/annotationWidth
iphoneScaleFactorLongitude = deviceScreenHeight/annotationHeight

filterAnnotations method:


-(void)filterAnnotations:(NSArray *)placesToFilter{
    float latDelta=myMapView.region.span.latitudeDelta/iphoneScaleFactorLatitude;
    float longDelta=myMapView.region.span.longitudeDelta/iphoneScaleFactorLongitude;

    NSMutableArray *shopsToShow=[[NSMutableArray alloc] initWithCapacity:0];

    for (int i=0; i<[placesToFilter count]; i++) {
        MyPlace *checkingLocation=[placesToFilter objectAtIndex:i];
        CLLocationDegrees latitude = [checkingLocation getCoordinate].latitude;
        CLLocationDegrees longitude = [checkingLocation getCoordinate].longitude;

        bool found=FALSE;
        for (MyPlace *tempPlacemark in shopsToShow) {
            if(fabs([tempPlacemark getCoordinate].latitude-latitude) < latDelta &&
               fabs([tempPlacemark getCoordinate].longitude-longitude) <longDelta ){
                [myMapView removeAnnotation:checkingLocation];
                found=TRUE;
                break;
            }
        }
        if (!found) {
            [shopsToShow addObject:checkingLocation];
            [myMapView addAnnotation:checkingLocation];
        }

    }
    [shopsToShow release];
}

And resulting map view after exchanging methods that populate map:

So now every icon represents a group of map locations. As we mentioned before user can zoom in to see more details, that is why we implement following MKMapViewDelegate method:


-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated{
    if (zoomLevel!=mapView.region.span.longitudeDelta) {
        [self filterAnnotations:places];
        zoomLevel=mapView.region.span.longitudeDelta;
    }
}

And pins are recalculated and placed again on map view.

What is missing?

For the next phase we should implement different pin icons for annotations that show groups of places, and animate pin grouping and ungrouping as map in Photos.app animates.

You can track this project on GitHub following this link.

0 comments