Breadcrumb/CrumbPath.m

/*
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 See LICENSE.txt for this sample’s licensing information
 
 */
 
#import "CrumbPath.h"
 
#import <pthread.h> // for pthread_rwlock_init
 
 
#define MINIMUM_DELTA_METERS 10.0
 
@interface CrumbPath ()
 
// Updated by -addCoordinate:boundingMapRectChanged: if needed to contain the new coordinate.
@property (nonatomic, readwrite) MKMapRect boundingMapRect;
 
@property MKMapPoint *pointBuffer;
@property NSUInteger pointCount;
@property NSUInteger pointBufferCapacity;
 
@property pthread_rwlock_t rwLock;
 
@end
 
 
#pragma mark -
 
@implementation CrumbPath
 
- (id)initWithCenterCoordinate:(CLLocationCoordinate2D)coord
{
    self = [super init];
    if (self != nil)
    {
        // Initialize point storage and place this first coordinate in it
        _pointBufferCapacity = 1000;
        _pointBuffer = malloc(sizeof(MKMapPoint) * self.pointBufferCapacity);
        MKMapPoint origin = MKMapPointForCoordinate(coord);
        self.pointBuffer[0] = origin;
        _pointCount = 1;
        
        // Default -boundingMapRect size is 1km^2 centered on coord
        double oneKilometerInMapPoints = 1000 * MKMapPointsPerMeterAtLatitude(coord.latitude);
        MKMapSize oneSquareKilometer = {oneKilometerInMapPoints, oneKilometerInMapPoints};
        _boundingMapRect = (MKMapRect){origin, oneSquareKilometer};
        
        // Clamp the rect to be within the world
        _boundingMapRect = MKMapRectIntersection(self.boundingMapRect, MKMapRectWorld);
        
        // Initialize read-write lock for drawing and updates
        //
        // We didn't use this lock during this method because
        // it's our user's responsibility not to use us before
        // -init completes.
        pthread_rwlock_init(&_rwLock, NULL);
    }
    return self;
}
 
- (void)dealloc
{
    free(_pointBuffer);
    pthread_rwlock_destroy(&_rwLock);
}
 
- (CLLocationCoordinate2D)coordinate
{
    __block CLLocationCoordinate2D centerCoordinate;
    [self readPointsWithBlockAndWait:^(MKMapPoint *pointsArray, NSUInteger pointsCount) {
        centerCoordinate = MKCoordinateForMapPoint(pointsArray[0]);
    }];
    return centerCoordinate;
}
 
- (void)readPointsWithBlockAndWait:(void (^)(MKMapPoint *pointsArray, NSUInteger pointArrayCount))block
{
    // Acquire the write lock so the list of points isn't changed while we read it
    pthread_rwlock_wrlock(&_rwLock);
    block(self.pointBuffer, self.pointCount);
    pthread_rwlock_unlock(&_rwLock);
}
 
- (MKMapRect)growOverlayBounds:(MKMapRect)overlayBounds toInclude:(MKMapRect)otherRect
{
    // The -boundingMapRect we choose was too small.
    // We grow it to be both rects, plus about
    // an extra kilometer in every direction that was too small before.
    // Usually the crumb-trail will keep growing in the direction it grew before
    // so this minimizes having to regrow, without growing off-trail.
    
    MKMapRect grownBounds = MKMapRectUnion(overlayBounds, otherRect);
    
    // Pedantically, to grow the overlay by one real kilometer, we would need to
    // grow different sides by a different number of map points, to account for
    // the number of map points per meter changing with latitude.
    // But we don't need to be exact. The center of the rect that ran over
    // is a good enough estimate for where we'll be growing the overlay.
    
    double oneKilometerInMapPoints = 1000*MKMapPointsPerMeterAtLatitude(MKCoordinateForMapPoint(otherRect.origin).latitude);
    
    // Grow by an extra kilometer in the direction of each overrun.
    if (MKMapRectGetMinY(otherRect) < MKMapRectGetMinY(overlayBounds))
    {
        grownBounds.origin.y -= oneKilometerInMapPoints;
        grownBounds.size.height += oneKilometerInMapPoints;
    }
    if (MKMapRectGetMaxY(otherRect) > MKMapRectGetMaxY(overlayBounds))
    {
        grownBounds.size.height += oneKilometerInMapPoints;
    }
    if (MKMapRectGetMinX(otherRect) < MKMapRectGetMinX(overlayBounds))
    {
        grownBounds.origin.x -= oneKilometerInMapPoints;
        grownBounds.size.width += oneKilometerInMapPoints;
    }
    if (MKMapRectGetMaxX(otherRect) > MKMapRectGetMaxX(overlayBounds))
    {
        grownBounds.size.width += oneKilometerInMapPoints;
    }
    
    // Clip to world size
    grownBounds = MKMapRectIntersection(grownBounds, MKMapRectWorld);
    
    return grownBounds;
}
 
- (MKMapRect)mapRectContainingPoint:(MKMapPoint)p1 andPoint:(MKMapPoint)p2
{
    MKMapSize pointSize = {0,0};
    MKMapRect newPointRect = (MKMapRect){p1,pointSize};
    MKMapRect prevPointRect = (MKMapRect){p2,pointSize};
    return MKMapRectUnion(newPointRect, prevPointRect);
}
 
- (MKMapRect)addCoordinate:(CLLocationCoordinate2D)newCoord boundingMapRectChanged:(BOOL *)boundingMapRectChangedOut
{
    // Acquire the write lock because we are going to be changing the list of points
    pthread_rwlock_wrlock(&_rwLock);
    
    //Assume no changes until we make one.
    BOOL boundingMapRectChanged = NO;
    MKMapRect updateRect = MKMapRectNull;
    
    // Convert to map space
    MKMapPoint newPoint = MKMapPointForCoordinate(newCoord);
    
    // Get the distance between this new point and the previous point.
    MKMapPoint prevPoint = self.pointBuffer[self.pointCount - 1];
    CLLocationDistance metersApart = MKMetersBetweenMapPoints(newPoint, prevPoint);
    
    // Ignore the point if it's too close to the previous one.
    if (metersApart > MINIMUM_DELTA_METERS)
    {
        // Grow the points buffer if necessary
        if (self.pointBufferCapacity == self.pointCount)
        {
            _pointBufferCapacity *= 2;
            _pointBuffer = realloc(self.pointBuffer, sizeof(MKMapPoint) * self.pointBufferCapacity);
        }
        
        // Add the new point to the points buffer
        self.pointBuffer[self.pointCount] = newPoint;
        _pointCount++;
        
        // Compute MKMapRect bounding prevPoint and newPoint
        updateRect = [self mapRectContainingPoint:newPoint andPoint:prevPoint];
        
        //Update the -boundingMapRect to hold the new point if needed
        MKMapRect overlayBounds = self.boundingMapRect;
        if (NO == MKMapRectContainsRect(overlayBounds,updateRect))
        {
            self.boundingMapRect = [self growOverlayBounds:overlayBounds toInclude:updateRect];
            boundingMapRectChanged = YES;
        }
    }
    
    // Report if -boundingMapRect changed
    if (boundingMapRectChangedOut)
    {
        *boundingMapRectChangedOut = boundingMapRectChanged;
    }
    
    pthread_rwlock_unlock(&_rwLock);
    
    return updateRect;
}
 
@end