From e0fb7ec2d26c6cbc646a8f7dde9bae47c16cd08f Mon Sep 17 00:00:00 2001
From: Thomas Rasch <thomas@fook.de>
Date: Wed, 20 Jun 2012 17:24:01 +0200
Subject: [PATCH] o Added the RMShape implementation from @plancalculus which will replace RMPath (addresses #29)

---
 MapView/Map/RMMapScrollView.h             |   4 ++--
 MapView/Map/RMMapScrollView.m             |  14 +++++++-------
 MapView/Map/RMMapView.h                   |   4 ++--
 MapView/Map/RMMapView.m                   |  36 +++++++++++++++++++++++++++++-------
 MapView/Map/RMShape.h                     |  82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 MapView/Map/RMShape.m                     | 444 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 MapView/MapView.xcodeproj/project.pbxproj |   8 ++++++++
 7 files changed, 574 insertions(+), 18 deletions(-)
 create mode 100644 MapView/Map/RMShape.h
 create mode 100644 MapView/Map/RMShape.m

diff --git a/MapView/Map/RMMapScrollView.h b/MapView/Map/RMMapScrollView.h
index 6fc2023..b81bc7f 100644
--- a/MapView/Map/RMMapScrollView.h
+++ b/MapView/Map/RMMapScrollView.h
@@ -10,7 +10,7 @@
 
 @class RMMapScrollView;
 
-@protocol UIScrollViewConstraintsDelegate <NSObject>
+@protocol RMMapScrollViewDelegate <NSObject>
 
 - (CGPoint)scrollView:(RMMapScrollView *)aScrollView correctedOffsetForContentOffset:(CGPoint)aContentOffset;
 - (CGSize)scrollView:(RMMapScrollView *)aScrollView correctedSizeForContentSize:(CGSize)aContentSize;
@@ -19,6 +19,6 @@
 
 @interface RMMapScrollView : UIScrollView
 
-@property (nonatomic, assign) id <UIScrollViewConstraintsDelegate> constraintsDelegate;
+@property (nonatomic, assign) id <RMMapScrollViewDelegate> mapScrollViewDelegate;
 
 @end
diff --git a/MapView/Map/RMMapScrollView.m b/MapView/Map/RMMapScrollView.m
index c4040ea..d62fa09 100644
--- a/MapView/Map/RMMapScrollView.m
+++ b/MapView/Map/RMMapScrollView.m
@@ -10,28 +10,28 @@
 
 @implementation RMMapScrollView
 
-@synthesize constraintsDelegate;
+@synthesize mapScrollViewDelegate;
 
 - (void)setContentOffset:(CGPoint)contentOffset
 {
-    if (self.constraintsDelegate)
-        contentOffset = [self.constraintsDelegate scrollView:self correctedOffsetForContentOffset:contentOffset];
+    if (self.mapScrollViewDelegate)
+        contentOffset = [self.mapScrollViewDelegate scrollView:self correctedOffsetForContentOffset:contentOffset];
 
     [super setContentOffset:contentOffset];
 }
 
 - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
 {
-    if (self.constraintsDelegate)
-        contentOffset = [self.constraintsDelegate scrollView:self correctedOffsetForContentOffset:contentOffset];
+    if (self.mapScrollViewDelegate)
+        contentOffset = [self.mapScrollViewDelegate scrollView:self correctedOffsetForContentOffset:contentOffset];
 
     [super setContentOffset:contentOffset animated:animated];
 }
 
 - (void)setContentSize:(CGSize)contentSize
 {
-    if (self.constraintsDelegate)
-        contentSize = [self.constraintsDelegate scrollView:self correctedSizeForContentSize:contentSize];
+    if (self.mapScrollViewDelegate)
+        contentSize = [self.mapScrollViewDelegate scrollView:self correctedSizeForContentSize:contentSize];
 
     [super setContentSize:contentSize];
 }
diff --git a/MapView/Map/RMMapView.h b/MapView/Map/RMMapView.h
index fdfc614..ee71cfc 100644
--- a/MapView/Map/RMMapView.h
+++ b/MapView/Map/RMMapView.h
@@ -62,10 +62,10 @@ typedef enum {
 
 @protocol RMMercatorToTileProjection;
 @protocol RMTileSource;
-@protocol UIScrollViewConstraintsDelegate;
+@protocol RMMapScrollViewDelegate;
 
 @interface RMMapView : UIView <UIScrollViewDelegate, RMMapOverlayViewDelegate,
-                               RMMapTiledLayerViewDelegate, UIScrollViewConstraintsDelegate>
+                               RMMapTiledLayerViewDelegate, RMMapScrollViewDelegate>
 {
     id <RMMapViewDelegate> delegate;
 
diff --git a/MapView/Map/RMMapView.m b/MapView/Map/RMMapView.m
index 1e900c8..4289a1e 100644
--- a/MapView/Map/RMMapView.m
+++ b/MapView/Map/RMMapView.m
@@ -33,6 +33,7 @@
 #import "RMProjection.h"
 #import "RMMarker.h"
 #import "RMPath.h"
+#import "RMShape.h"
 #import "RMAnnotation.h"
 #import "RMQuadTree.h"
 
@@ -101,6 +102,7 @@
 
     float _lastZoom;
     CGPoint _lastContentOffset, _accumulatedDelta;
+    CGSize _lastContentSize;
     BOOL _mapScrollViewIsZooming;
 
     BOOL _enableDragging, _enableBouncing;
@@ -465,7 +467,7 @@
     _constrainMovement = YES;
     _constrainingProjectedBounds = RMProjectedRectMake(southWest.x, southWest.y, northEast.x - southWest.x, northEast.y - southWest.y);
 
-    mapScrollView.constraintsDelegate = self;
+    mapScrollView.mapScrollViewDelegate = self;
 }
 
 #pragma mark -
@@ -976,10 +978,10 @@
     _lastZoom = [self zoom];
     _lastContentOffset = mapScrollView.contentOffset;
     _accumulatedDelta = CGPointMake(0.0, 0.0);
+    _lastContentSize = mapScrollView.contentSize;
 
     [mapScrollView addObserver:self forKeyPath:@"contentOffset" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
-    if (_constrainMovement)
-        mapScrollView.constraintsDelegate = self;
+    mapScrollView.mapScrollViewDelegate = self;
 
     mapScrollView.zoomScale = exp2f([self zoom]);
 
@@ -1240,8 +1242,27 @@
     NSValue *oldValue = [change objectForKey:NSKeyValueChangeOldKey],
             *newValue = [change objectForKey:NSKeyValueChangeNewKey];
 
-    if (CGPointEqualToPoint([oldValue CGPointValue], [newValue CGPointValue]))
+    CGPoint oldContentOffset = [oldValue CGPointValue],
+            newContentOffset = [newValue CGPointValue];
+
+    if (CGPointEqualToPoint(oldContentOffset, newContentOffset))
+        return;
+
+    // The first offset during zooming out (animated) is always garbage
+    if (_mapScrollViewIsZooming == YES &&
+        mapScrollView.zooming == NO &&
+        _lastContentSize.width > mapScrollView.contentSize.width &&
+        (newContentOffset.y - oldContentOffset.y) == 0.0)
+    {
+        _lastContentOffset = mapScrollView.contentOffset;
+        _lastContentSize = mapScrollView.contentSize;
+
         return;
+    }
+
+//    RMLog(@"contentOffset: {%.0f,%.0f} -> {%.1f,%.1f} (%.0f,%.0f)", oldContentOffset.x, oldContentOffset.y, newContentOffset.x, newContentOffset.y, newContentOffset.x - oldContentOffset.x, newContentOffset.y - oldContentOffset.y);
+//    RMLog(@"contentSize: {%.0f,%.0f} -> {%.0f,%.0f}", _lastContentSize.width, _lastContentSize.height, mapScrollView.contentSize.width, mapScrollView.contentSize.height);
+//    RMLog(@"isZooming: %d, scrollview.zooming: %d", _mapScrollViewIsZooming, mapScrollView.zooming);
 
     RMProjectedRect planetBounds = projection.planetBounds;
     metersPerPixel = planetBounds.size.width / mapScrollView.contentSize.width;
@@ -1274,11 +1295,12 @@
     }
     else
     {
-        [self correctPositionOfAllAnnotationsIncludingInvisibles:NO animated:YES];
+        [self correctPositionOfAllAnnotationsIncludingInvisibles:NO animated:(_mapScrollViewIsZooming && !mapScrollView.zooming)];
         _lastZoom = zoom;
     }
 
     _lastContentOffset = mapScrollView.contentOffset;
+    _lastContentSize = mapScrollView.contentSize;
 
     // Don't do anything stupid here or your scrolling experience will suck
     if (_delegateHasMapViewRegionDidChange)
@@ -1702,9 +1724,9 @@
     CGPoint newPosition = CGPointMake((normalizedProjectedPoint.x / metersPerPixel) - mapScrollView.contentOffset.x,
                                       mapScrollView.contentSize.height - (normalizedProjectedPoint.y / metersPerPixel) - mapScrollView.contentOffset.y);
 
-    [annotation setPosition:newPosition animated:animated];
-
 //    RMLog(@"Change annotation at {%f,%f} in mapView {%f,%f}", annotation.position.x, annotation.position.y, mapScrollView.contentSize.width, mapScrollView.contentSize.height);
+
+    [annotation setPosition:newPosition animated:animated];
 }
 
 - (void)correctPositionOfAllAnnotationsIncludingInvisibles:(BOOL)correctAllAnnotations animated:(BOOL)animated
diff --git a/MapView/Map/RMShape.h b/MapView/Map/RMShape.h
new file mode 100644
index 0000000..b6d0e80
--- /dev/null
+++ b/MapView/Map/RMShape.h
@@ -0,0 +1,82 @@
+//
+//  RMShape.h
+//
+// Copyright (c) 2008-2012, Route-Me Contributors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright notice, this
+//   list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright notice,
+//   this list of conditions and the following disclaimer in the documentation
+//   and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+#import <UIKit/UIKit.h>
+
+#import "RMFoundation.h"
+#import "RMMapLayer.h"
+
+@class RMMapView;
+
+@interface RMShape : RMMapLayer
+{
+    CGRect pathBoundingBox;
+
+    /// Width of the line, in pixels
+    float lineWidth;
+
+    // Line dash style
+    NSArray *lineDashLengths;
+    CGFloat lineDashPhase;
+
+    BOOL scaleLineWidth;
+    BOOL scaleLineDash; // if YES line dashes will be scaled to keep a constant size if the layer is zoomed
+}
+
+- (id)initWithView:(RMMapView *)aMapView;
+
+@property (nonatomic, retain) NSString *fillRule;
+@property (nonatomic, retain) NSString *lineCap;
+@property (nonatomic, retain) NSString *lineJoin;
+@property (nonatomic, retain) UIColor *lineColor;
+@property (nonatomic, retain) UIColor *fillColor;
+
+@property (nonatomic, assign) NSArray *lineDashLengths;
+@property (nonatomic, assign) CGFloat lineDashPhase;
+@property (nonatomic, assign) BOOL scaleLineDash;
+@property (nonatomic, assign) float lineWidth;
+@property (nonatomic, assign) BOOL	scaleLineWidth;
+
+@property (nonatomic, readonly) CGRect pathBoundingBox;
+
+- (void)moveToProjectedPoint:(RMProjectedPoint)projectedPoint;
+- (void)moveToScreenPoint:(CGPoint)point;
+- (void)moveToCoordinate:(CLLocationCoordinate2D)coordinate;
+
+- (void)addLineToProjectedPoint:(RMProjectedPoint)projectedPoint;
+- (void)addLineToScreenPoint:(CGPoint)point;
+- (void)addLineToCoordinate:(CLLocationCoordinate2D)coordinate;
+
+// Change the path without recalculating the geometry (performance!)
+- (void)performBatchOperations:(void (^)(RMShape *aPath))block;
+
+/// This closes the path, connecting the last point to the first.
+/// After this action, no further points can be added to the path.
+/// There is no requirement that a path be closed.
+- (void)closePath;
+
+@end
diff --git a/MapView/Map/RMShape.m b/MapView/Map/RMShape.m
new file mode 100644
index 0000000..8ed7d0f
--- /dev/null
+++ b/MapView/Map/RMShape.m
@@ -0,0 +1,444 @@
+///
+//  RMShape.m
+//
+// Copyright (c) 2008-2012, Route-Me Contributors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright notice, this
+//   list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright notice,
+//   this list of conditions and the following disclaimer in the documentation
+//   and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+#import "RMShape.h"
+#import "RMPixel.h"
+#import "RMProjection.h"
+#import "RMMapView.h"
+#import "RMAnnotation.h"
+
+@implementation RMShape
+{
+    BOOL isFirstPoint, ignorePathUpdates;
+    float lastScale;
+
+    CGRect nonClippedBounds;
+    CGRect previousBounds;
+
+    CAShapeLayer *shapeLayer;
+    UIBezierPath *bezierPath;
+
+    RMMapView *mapView;
+}
+
+@synthesize scaleLineWidth;
+@synthesize lineDashLengths;
+@synthesize scaleLineDash;
+@synthesize pathBoundingBox;
+
+#define kDefaultLineWidth 2.0
+
+- (id)initWithView:(RMMapView *)aMapView
+{
+    if (!(self = [super init]))
+        return nil;
+
+    mapView = aMapView;
+
+    bezierPath = [[UIBezierPath alloc] init];
+    lineWidth = kDefaultLineWidth;
+    ignorePathUpdates = NO;
+
+    shapeLayer = [[CAShapeLayer alloc] init];
+    shapeLayer.rasterizationScale = [[UIScreen mainScreen] scale];
+    shapeLayer.lineWidth = lineWidth;
+    shapeLayer.lineCap = kCALineCapButt;
+    shapeLayer.lineJoin = kCALineJoinMiter;
+    shapeLayer.strokeColor = [UIColor blackColor].CGColor;
+    shapeLayer.fillColor = [UIColor clearColor].CGColor;
+    [self addSublayer:shapeLayer];
+
+    pathBoundingBox = CGRectZero;
+    nonClippedBounds = CGRectZero;
+    previousBounds = CGRectZero;
+    lastScale = 0.0;
+
+    self.masksToBounds = YES;
+
+    scaleLineWidth = NO;
+    scaleLineDash = NO;
+    isFirstPoint = YES;
+
+    [(id)self setValue:[[UIScreen mainScreen] valueForKey:@"scale"] forKey:@"contentsScale"];
+
+    return self;
+}
+
+- (void)dealloc
+{
+    mapView = nil;
+    [bezierPath release]; bezierPath = nil;
+    [shapeLayer release]; shapeLayer = nil;
+    [super dealloc];
+}
+
+- (id <CAAction>)actionForKey:(NSString *)key
+{
+    return nil;
+}
+
+#pragma mark -
+
+- (void)recalculateGeometryAnimated:(BOOL)animated
+{
+    if (ignorePathUpdates)
+        return;
+
+    float scale = 1.0f / [mapView metersPerPixel];
+
+    // we have to calculate the scaledLineWidth even if scalling did not change
+    // as the lineWidth might have changed
+    float scaledLineWidth;
+
+    if (scaleLineWidth)
+        scaledLineWidth = lineWidth * scale;
+    else
+        scaledLineWidth = lineWidth;
+
+    shapeLayer.lineWidth = scaledLineWidth;
+
+    if (lineDashLengths)
+    {
+        if (scaleLineDash)
+        {
+            NSMutableArray *scaledLineDashLengths = [NSMutableArray array];
+
+            for (NSNumber *lineDashLength in lineDashLengths)
+            {
+                [scaledLineDashLengths addObject:[NSNumber numberWithFloat:lineDashLength.floatValue * scale]];
+            }
+
+            shapeLayer.lineDashPattern = scaledLineDashLengths;
+        }
+        else
+        {
+            shapeLayer.lineDashPattern = lineDashLengths;
+        }
+    }
+
+    // we are about to overwrite nonClippedBounds, therefore we save the old value
+    CGRect previousNonClippedBounds = nonClippedBounds;
+
+    if (scale != lastScale)
+    {
+        lastScale = scale;
+
+        CGAffineTransform scaling = CGAffineTransformMakeScale(scale, scale);
+        UIBezierPath *scaledPath = [bezierPath copy];
+        [scaledPath applyTransform:scaling];
+
+        if (animated)
+        {
+            CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
+            animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+            animation.repeatCount = 0;
+            animation.autoreverses = NO;
+            animation.fromValue = (id) shapeLayer.path;
+            animation.toValue = (id) scaledPath.CGPath;
+            [shapeLayer addAnimation:animation forKey:@"animatePath"];
+        }
+
+        shapeLayer.path = scaledPath.CGPath;
+
+        // calculate the bounds of the scaled path
+        CGRect boundsInMercators = scaledPath.bounds;
+        nonClippedBounds = CGRectInset(boundsInMercators, -scaledLineWidth, -scaledLineWidth);
+
+        [scaledPath release];
+    }
+
+    // if the path is not scaled, nonClippedBounds stay the same as in the previous invokation
+
+    // Clip bound rect to screen bounds.
+    // If bounds are not clipped, they won't display when you zoom in too much.
+
+    CGRect screenBounds = [mapView frame];
+
+    // we start with the non-clipped bounds and clip them
+    CGRect clippedBounds = nonClippedBounds;
+
+    float offset;
+    const float outset = 150.0f; // provides a buffer off screen edges for when path is scaled or moved
+
+    CGPoint newPosition = self.annotation.position;
+
+//    RMLog(@"x:%f y:%f screen bounds: %f %f %f %f", newPosition.x, newPosition.y,  screenBounds.origin.x, screenBounds.origin.y, screenBounds.size.width, screenBounds.size.height);
+
+    // Clip top
+    offset = newPosition.y + clippedBounds.origin.y - screenBounds.origin.y + outset;
+    if (offset < 0.0f)
+    {
+        clippedBounds.origin.y -= offset;
+        clippedBounds.size.height += offset;
+    }
+
+    // Clip left
+    offset = newPosition.x + clippedBounds.origin.x - screenBounds.origin.x + outset;
+    if (offset < 0.0f)
+    {
+        clippedBounds.origin.x -= offset;
+        clippedBounds.size.width += offset;
+    }
+
+    // Clip bottom
+    offset = newPosition.y + clippedBounds.origin.y + clippedBounds.size.height - screenBounds.origin.y - screenBounds.size.height - outset;
+    if (offset > 0.0f)
+    {
+        clippedBounds.size.height -= offset;
+    }
+
+    // Clip right
+    offset = newPosition.x + clippedBounds.origin.x + clippedBounds.size.width - screenBounds.origin.x - screenBounds.size.width - outset;
+    if (offset > 0.0f)
+    {
+        clippedBounds.size.width -= offset;
+    }
+
+    if (animated)
+    {
+        CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
+        positionAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+        positionAnimation.repeatCount = 0;
+        positionAnimation.autoreverses = NO;
+        positionAnimation.fromValue = [NSValue valueWithCGPoint:self.position];
+        positionAnimation.toValue = [NSValue valueWithCGPoint:newPosition];
+        [self addAnimation:positionAnimation forKey:@"animatePosition"];
+    }
+
+    super.position = newPosition;
+
+    // bounds are animated non-clipped but set with clipping
+
+    CGPoint previousNonClippedAnchorPoint = CGPointMake(-previousNonClippedBounds.origin.x / previousNonClippedBounds.size.width,
+                                                        -previousNonClippedBounds.origin.y / previousNonClippedBounds.size.height);
+    CGPoint nonClippedAnchorPoint = CGPointMake(-nonClippedBounds.origin.x / nonClippedBounds.size.width,
+                                                -nonClippedBounds.origin.y / nonClippedBounds.size.height);
+    CGPoint clippedAnchorPoint = CGPointMake(-clippedBounds.origin.x / clippedBounds.size.width,
+                                             -clippedBounds.origin.y / clippedBounds.size.height);
+
+    if (animated)
+    {
+        CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
+        boundsAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+        boundsAnimation.repeatCount = 0;
+        boundsAnimation.autoreverses = NO;
+        boundsAnimation.fromValue = [NSValue valueWithCGRect:previousNonClippedBounds];
+        boundsAnimation.toValue = [NSValue valueWithCGRect:nonClippedBounds];
+        [self addAnimation:boundsAnimation forKey:@"animateBounds"];
+    }
+
+    self.bounds = clippedBounds;
+    previousBounds = clippedBounds;
+
+    // anchorPoint is animated non-clipped but set with clipping
+    if (animated)
+    {
+        CABasicAnimation *anchorPointAnimation = [CABasicAnimation animationWithKeyPath:@"anchorPoint"];
+        anchorPointAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+        anchorPointAnimation.repeatCount = 0;
+        anchorPointAnimation.autoreverses = NO;
+        anchorPointAnimation.fromValue = [NSValue valueWithCGPoint:previousNonClippedAnchorPoint];
+        anchorPointAnimation.toValue = [NSValue valueWithCGPoint:nonClippedAnchorPoint];
+        [self addAnimation:anchorPointAnimation forKey:@"animateAnchorPoint"];
+    }
+
+    self.anchorPoint = clippedAnchorPoint;
+}
+
+#pragma mark -
+
+- (void)addPointToProjectedPoint:(RMProjectedPoint)point withDrawing:(BOOL)isDrawing
+{
+    if (isFirstPoint)
+    {
+        isFirstPoint = FALSE;
+        projectedLocation = point;
+
+        self.position = [mapView projectedPointToPixel:projectedLocation];
+
+        [bezierPath moveToPoint:CGPointMake(0.0f, 0.0f)];
+    }
+    else
+    {
+        point.x = point.x - projectedLocation.x;
+        point.y = point.y - projectedLocation.y;
+
+        if (isDrawing)
+            [bezierPath addLineToPoint:CGPointMake(point.x, -point.y)];
+        else
+            [bezierPath moveToPoint:CGPointMake(point.x, -point.y)];
+
+        lastScale = 0.0;
+        [self recalculateGeometryAnimated:NO];
+    }
+
+    [self setNeedsDisplay];
+}
+
+- (void)moveToProjectedPoint:(RMProjectedPoint)projectedPoint
+{
+    [self addPointToProjectedPoint:projectedPoint withDrawing:NO];
+}
+
+- (void)moveToScreenPoint:(CGPoint)point
+{
+    RMProjectedPoint mercator = [mapView pixelToProjectedPoint:point];
+    [self moveToProjectedPoint:mercator];
+}
+
+- (void)moveToCoordinate:(CLLocationCoordinate2D)coordinate
+{
+    RMProjectedPoint mercator = [[mapView projection] coordinateToProjectedPoint:coordinate];
+    [self moveToProjectedPoint:mercator];
+}
+
+- (void)addLineToProjectedPoint:(RMProjectedPoint)projectedPoint
+{
+    [self addPointToProjectedPoint:projectedPoint withDrawing:YES];
+}
+
+- (void)addLineToScreenPoint:(CGPoint)point
+{
+    RMProjectedPoint mercator = [mapView pixelToProjectedPoint:point];
+    [self addLineToProjectedPoint:mercator];
+}
+
+- (void)addLineToCoordinate:(CLLocationCoordinate2D)coordinate
+{
+    RMProjectedPoint mercator = [[mapView projection] coordinateToProjectedPoint:coordinate];
+    [self addLineToProjectedPoint:mercator];
+}
+
+- (void)performBatchOperations:(void (^)(RMShape *aPath))block
+{
+    ignorePathUpdates = YES;
+    block(self);
+    ignorePathUpdates = NO;
+
+    lastScale = 0.0;
+    [self recalculateGeometryAnimated:NO];
+}
+
+#pragma mark - Accessors
+
+- (void)closePath
+{
+    [bezierPath closePath];
+}
+
+- (float)lineWidth
+{
+    return lineWidth;
+}
+
+- (void)setLineWidth:(float)newLineWidth
+{
+    lineWidth = newLineWidth;
+
+    lastScale = 0.0;
+    [self recalculateGeometryAnimated:NO];
+}
+
+- (NSString *)lineCap
+{
+    return shapeLayer.lineCap;
+}
+
+- (void)setLineCap:(NSString *)newLineCap
+{
+    shapeLayer.lineCap = newLineCap;
+    [self setNeedsDisplay];
+}
+
+- (NSString *)lineJoin
+{
+    return shapeLayer.lineJoin;
+}
+
+- (void)setLineJoin:(NSString *)newLineJoin
+{
+    shapeLayer.lineJoin = newLineJoin;
+    [self setNeedsDisplay];
+}
+
+- (UIColor *)lineColor
+{
+    return [UIColor colorWithCGColor:shapeLayer.strokeColor];
+}
+
+- (void)setLineColor:(UIColor *)aLineColor
+{
+    if (shapeLayer.strokeColor != aLineColor.CGColor)
+    {
+        shapeLayer.strokeColor = aLineColor.CGColor;
+        [self setNeedsDisplay];
+    }
+}
+
+- (UIColor *)fillColor
+{
+    return [UIColor colorWithCGColor:shapeLayer.fillColor];
+}
+
+- (void)setFillColor:(UIColor *)aFillColor
+{
+    if (shapeLayer.fillColor != aFillColor.CGColor)
+    {
+        shapeLayer.fillColor = aFillColor.CGColor;
+        [self setNeedsDisplay];
+    }
+}
+
+- (NSString *)fillRule
+{
+    return shapeLayer.fillRule;
+}
+
+- (void)setFillRule:(NSString *)fillRule
+{
+    shapeLayer.fillRule = fillRule;
+}
+
+- (CGFloat)lineDashPhase
+{
+    return shapeLayer.lineDashPhase;
+}
+
+- (void)setLineDashPhase:(CGFloat)dashPhase
+{
+    shapeLayer.lineDashPhase = dashPhase;
+}
+
+- (void)setPosition:(CGPoint)newPosition animated:(BOOL)animated
+{
+    if (CGPointEqualToPoint(newPosition, super.position) && CGRectEqualToRect(self.bounds, previousBounds))
+        return;
+
+    [self recalculateGeometryAnimated:animated];
+}
+
+@end
diff --git a/MapView/MapView.xcodeproj/project.pbxproj b/MapView/MapView.xcodeproj/project.pbxproj
index 290a32e..23b38a5 100755
--- a/MapView/MapView.xcodeproj/project.pbxproj
+++ b/MapView/MapView.xcodeproj/project.pbxproj
@@ -53,6 +53,8 @@
 		16EC85D7133CA6C300219947 /* RMCacheObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 16EC85D1133CA6C300219947 /* RMCacheObject.m */; };
 		16F3581B15864135003A3AD9 /* RMMapScrollView.h in Headers */ = {isa = PBXBuildFile; fileRef = 16F3581915864135003A3AD9 /* RMMapScrollView.h */; };
 		16F3581C15864135003A3AD9 /* RMMapScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F3581A15864135003A3AD9 /* RMMapScrollView.m */; };
+		16F98C961590CFF000FF90CE /* RMShape.h in Headers */ = {isa = PBXBuildFile; fileRef = 16F98C941590CFF000FF90CE /* RMShape.h */; };
+		16F98C971590CFF000FF90CE /* RMShape.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F98C951590CFF000FF90CE /* RMShape.m */; };
 		16FAB66413E03D55002F4E1C /* RMQuadTree.h in Headers */ = {isa = PBXBuildFile; fileRef = 16FAB66213E03D55002F4E1C /* RMQuadTree.h */; };
 		16FAB66513E03D55002F4E1C /* RMQuadTree.m in Sources */ = {isa = PBXBuildFile; fileRef = 16FAB66313E03D55002F4E1C /* RMQuadTree.m */; };
 		16FFF2CB14E3DBF700A170EC /* RMMapQuestOpenAerialSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 16FFF2C914E3DBF700A170EC /* RMMapQuestOpenAerialSource.h */; };
@@ -204,6 +206,8 @@
 		16EC85D1133CA6C300219947 /* RMCacheObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMCacheObject.m; sourceTree = "<group>"; };
 		16F3581915864135003A3AD9 /* RMMapScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapScrollView.h; sourceTree = "<group>"; };
 		16F3581A15864135003A3AD9 /* RMMapScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapScrollView.m; sourceTree = "<group>"; };
+		16F98C941590CFF000FF90CE /* RMShape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMShape.h; sourceTree = "<group>"; };
+		16F98C951590CFF000FF90CE /* RMShape.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMShape.m; sourceTree = "<group>"; };
 		16FAB66213E03D55002F4E1C /* RMQuadTree.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMQuadTree.h; sourceTree = "<group>"; };
 		16FAB66313E03D55002F4E1C /* RMQuadTree.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMQuadTree.m; sourceTree = "<group>"; };
 		16FFF2C914E3DBF700A170EC /* RMMapQuestOpenAerialSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapQuestOpenAerialSource.h; sourceTree = "<group>"; };
@@ -587,6 +591,8 @@
 				B8F3FC630EA2E792004D8F85 /* RMMarker.m */,
 				B8CEB1C30ED5A3480014C431 /* RMPath.h */,
 				B8CEB1C40ED5A3480014C431 /* RMPath.m */,
+				16F98C941590CFF000FF90CE /* RMShape.h */,
+				16F98C951590CFF000FF90CE /* RMShape.m */,
 				25757F4D1291C8640083D504 /* RMCircle.h */,
 				25757F4E1291C8640083D504 /* RMCircle.m */,
 			);
@@ -677,6 +683,7 @@
 				1607499514E120A100D535F5 /* RMGenericMapSource.h in Headers */,
 				16FFF2CB14E3DBF700A170EC /* RMMapQuestOpenAerialSource.h in Headers */,
 				16F3581B15864135003A3AD9 /* RMMapScrollView.h in Headers */,
+				16F98C961590CFF000FF90CE /* RMShape.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -895,6 +902,7 @@
 				1607499614E120A100D535F5 /* RMGenericMapSource.m in Sources */,
 				16FFF2CC14E3DBF700A170EC /* RMMapQuestOpenAerialSource.m in Sources */,
 				16F3581C15864135003A3AD9 /* RMMapScrollView.m in Sources */,
+				16F98C971590CFF000FF90CE /* RMShape.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
--
libgit2 0.24.0