Filling a map with locations in a iOS application

In many applications it is very common to need to fill a map with locations. Backbeam has a built-in "location" data type that you can use in your data model. When you use this data type Backbeam automatically creates special indexes for your data and supports to make geoqueries over them. Additionally in the control panel you will see an easy way to insert geolocated data into your database. For example you will be able to write an address and Backbeam will calculate the coordenates for that given address.

Well, let's see an example of how to use these geolocation capabilities in a iOS application. For this example we need a very simple data model. We have a "place" entity with at least two fields: a "name" (text) and a "location" (of type location). The "location" field name is not a very meaningful name but in other data models we could have a "location-start" and "location-end" or other things. For our example "location" is just easy to remember :)

Now what we need is a simple iOS app with a MKMapView. You probably should know how to do this. Create a MKMapView with code or with Interface Builder. Remember to add the MapKit Framework to your application in order to compile. Now we implement the MKMapViewDelegate protocol. Remember to set the mapView.delegate either with code or Interface Builder.

What we want know is to refresh the data each time the user moves the visible region of the map and fill it with the places in that region. For this kind of functionality we are going to make a "bounding" geoquery. We need to calculate the north-east and south-west coordinates. We calculate them and then we make the query to Backbeam.

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
    // Calculate the coordinates

    CGPoint nePoint = CGPointMake(self.mapView.bounds.origin.x + self.mapView.bounds.size.width, self.mapView.bounds.origin.y);
    CGPoint swPoint = CGPointMake((self.mapView.bounds.origin.x), (self.mapView.bounds.origin.y + self.mapView.bounds.size.height));
    
    CLLocationCoordinate2D neCoord;
    neCoord = [self.mapView convertPoint:nePoint toCoordinateFromView:self.mapView];
    
    CLLocationCoordinate2D swCoord;
    swCoord = [self.mapView convertPoint:swPoint toCoordinateFromView:self.mapView];
    
    double swlat = swCoord.latitude;
    double swlon = swCoord.longitude;
    double nelat = neCoord.latitude;
    double nelon = neCoord.longitude;
    
    // Make the query

    BBQuery *query = [Backbeam queryForEntity:@"place"];
    [query bounding:@"location" swlat:swlat swlon:swlon nelat:nelat nelon:nelon limit:300 success:^(NSArray *arr, NSInteger total, BOOL cached) {
        
        [self.mapView removeAnnotations:self.mapView.annotations];
        
        for (BBObject *object in arr) {
            BBLocation *location = [object locationForField:@"location"];
            MKPointAnnotation *ann = [[MKPointAnnotation alloc] init];
            ann.title = [object stringForField:@"name"];
            ann.coordinate = CLLocationCoordinate2DMake(location.latitude, location.longitude);
            [self.mapView addAnnotation:ann];
        }
        
    } failure:^(NSError* err) {
        NSLog(@"error %@", err);
    }];
}

This works well, however this approach has a problem. Each time we make a query all annotations are removed and inserted the new ones. But sometimes we are removing annotations and creating new ones that are exactly equal. This is a problem in one scenario: if the user selects an annotation and then moves the map, the selected annotation is removed and replaced by other annotation that is exactly equal but the selection is lost. This problem also arises when the user selects an annotation near to an edge: if the callout bubble needs more space to be shown the MapView is moved automatically in order the callout to fit. But due to this move the annotations are recalculated and the selected annotation is removed and the selection lost.

We have another problem related to the MapKit Framework itself. The annotations don't have a reference to the data model. In our case the annotations don't have a reference to the BBObject they are representing. So we are going to make a CustomAnnotation that we could reuse in any application:

//
//  CustomAnnotation.h
//

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

@interface CustomAnnotation : NSObject <MKAnnotation> {
    
}

@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *subtitle;

@property (nonatomic, strong) id object; // a reference to the data model

@end
The CustomAnnotation.m file is almost empty. Now let's change the mapView:regionDidChangeAnimated: method.
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
    // Calculate the coordinates
    
    CGPoint nePoint = CGPointMake(self.mapView.bounds.origin.x + self.mapView.bounds.size.width, self.mapView.bounds.origin.y);
    CGPoint swPoint = CGPointMake((self.mapView.bounds.origin.x), (self.mapView.bounds.origin.y + self.mapView.bounds.size.height));
    
    // Then transform those point into lat,lng values
    CLLocationCoordinate2D neCoord = [self.mapView convertPoint:nePoint toCoordinateFromView:self.mapView];
    CLLocationCoordinate2D swCoord = [self.mapView convertPoint:swPoint toCoordinateFromView:self.mapView];
    
    double swlat = swCoord.latitude;
    double swlon = swCoord.longitude;
    double nelat = neCoord.latitude;
    double nelon = neCoord.longitude;
    
    // Make the query
    
    static NSInteger MAX_LOCATIONS = 300;
    
    BBQuery *query = [Backbeam queryForEntity:@"place"];
    [query bounding:@"location" swlat:swlat swlon:swlon nelat:nelat nelon:nelon limit:MAX_LOCATIONS success:^(NSArray* arr, NSInteger total, BOOL cached) {
        
        NSMutableArray *returned = [[NSMutableArray alloc] initWithArray:arr];
        NSMutableArray *annotationsToRemove = [[NSMutableArray alloc] initWithCapacity:MAX_LOCATIONS];
        NSMutableArray *annotationsToAdd = [[NSMutableArray alloc] initWithCapacity:MAX_LOCATIONS];
        
        for (id<MKAnnotation> annotation in self.mapView.annotations) {
            if ([annotation isKindOfClass:[CustomAnnotation class]]) {
                CustomAnnotation *ann = (CustomAnnotation*) annotation;
                BOOL found = NO;
                for (BBObject *object in arr) {
                    BBObject *otherObject = (BBObject*) ann.object;
                    if ([object.identifier isEqualToString:otherObject.identifier]) {
                        // annotation already visible, do not add result
                        [returned removeObject:object];
                        found = YES;
                        break;
                    }
                }
                if (!found) {
                    // annotation no longer visible
                    [annotationsToRemove addObject:ann];
                }
            }
        }
        [self.mapView removeAnnotations:annotationsToRemove];
        
        // add results not visible yet
        for (BBObject *object in returned) {
            BBLocation *location = [object locationForField:@"location"];
            CustomAnnotation *ann = [[CustomAnnotation alloc] init];
            ann.title = [object stringForField:@"name"];
            ann.coordinate = CLLocationCoordinate2DMake(location.latitude, location.longitude);
            ann.object = object;
            [annotationsToAdd addObject:ann];
        }
        
        [self.mapView addAnnotations:annotationsToAdd];
        
    } failure:^(NSError* err) {
        NSLog(@"error %@", err);
    }];
}

This seems like a lot of code, but the only thing we are doing is just iterating the annotations that are already inserted in the map and removing them if they are no longer visible. And now, thanks to our CustomAnnotation we can access the data object in methods like mapView:annotationView:calloutAccessoryControlTapped: