Skip to content

Instantly share code, notes, and snippets.

@ns-1m
Forked from jan-christiansen/RMShape.h
Created May 24, 2012 04:57
Show Gist options
  • Save ns-1m/2779559 to your computer and use it in GitHub Desktop.
Save ns-1m/2779559 to your computer and use it in GitHub Desktop.
Implementation of RMPath using CAShapeLayer
//
// 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
{
BOOL isFirstPoint;
CGRect pathBoundingBox;
BOOL ignorePathUpdates;
CGRect previousBounds;
// Width of the line, in pixels
float lineWidth;
// Line dash style
NSArray *lineDashLengths;
CGFloat *_scaledLineDashLengths;
size_t _lineDashCount;
CGFloat lineDashPhase;
// Line shadow
CGFloat shadowBlur;
CGSize shadowOffset;
BOOL enableShadow;
BOOL scaleLineWidth;
BOOL scaleLineDash; // if YES line dashes will be scaled to keep a constant size if the layer is zoomed
float renderedScale;
RMMapView *mapView;
CAShapeLayer *shapeLayer;
UIBezierPath *bezierPath;
CGRect nonClippedBounds;
}
- (id)initWithView:(RMMapView *)aMapView;
@property (nonatomic, retain) NSString *fillRule;
@property (nonatomic, retain) NSString *lineCap;
@property (nonatomic, retain) NSString *lineJoin;
@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, retain) UIColor *lineColor;
@property (nonatomic, retain) UIColor *fillColor;
@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 *aShape))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;
- (void)recalculateGeometry;
@end
///
// 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
@synthesize scaleLineWidth;
@synthesize scaleLineDash;
@synthesize pathBoundingBox;
@synthesize lineDashLengths;
#define kDefaultLineWidth 2.0
- (id)initWithView:(RMMapView *)aMapView
{
if (!(self = [super init]))
return nil;
shapeLayer = [[CAShapeLayer alloc] init];
[self addSublayer:shapeLayer];
shapeLayer.shouldRasterize = YES;
mapView = aMapView;
bezierPath = [[UIBezierPath alloc] init];
pathBoundingBox = CGRectZero;
ignorePathUpdates = NO;
previousBounds = CGRectZero;
lineWidth = kDefaultLineWidth;
shapeLayer.lineWidth = kDefaultLineWidth;
shapeLayer.lineCap = kCALineCapButt;
shapeLayer.lineJoin = kCALineJoinMiter;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
self.masksToBounds = NO;
scaleLineWidth = NO;
scaleLineDash = NO;
isFirstPoint = YES;
if ([self respondsToSelector:@selector(setContentsScale:)])
[(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
{
// if ([key isEqualToString:@"position"] || [key isEqualToString:@"bounds"])
// return [super actionForKey:key];
// else
return nil;
}
#pragma mark -
- (void)recalculateGeometry
{
if (ignorePathUpdates)
return;
CGPoint newPosition = self.annotation.position;
float scale = 1.0f / [mapView metersPerPixel];
float scaledLineWidth;
CGRect pixelBounds, screenBounds;
float offset;
const float outset = 100.0f; // provides a buffer off screen edges for when path is scaled or moved
if (scaleLineWidth)
scaledLineWidth = lineWidth * scale;
else
scaledLineWidth = lineWidth;
// The bounds are actually in mercators...
/// \bug if "bounds are actually in mercators", shouldn't be using a CGRect
CGRect boundsInMercators = bezierPath.bounds;
boundsInMercators = CGRectInset(boundsInMercators, -scaledLineWidth, -scaledLineWidth);
pixelBounds = CGRectInset(boundsInMercators, -scaledLineWidth, -scaledLineWidth);
pixelBounds = RMScaleCGRectAboutPoint(pixelBounds, scale, CGPointZero);
CGRect previousNonClippedBounds = nonClippedBounds;
nonClippedBounds = pixelBounds;
// Clip bound rect to screen bounds.
// If bounds are not clipped, they won't display when you zoom in too much.
screenBounds = [mapView frame];
// RMLog(@"x:%f y:%f screen bounds: %f %f %f %f", myPosition.x, myPosition.y, screenBounds.origin.x, screenBounds.origin.y, screenBounds.size.width, screenBounds.size.height);
// Clip top
offset = newPosition.y + pixelBounds.origin.y - screenBounds.origin.y + outset;
if (offset < 0.0f)
{
pixelBounds.origin.y -= offset;
pixelBounds.size.height += offset;
}
// Clip left
offset = newPosition.x + pixelBounds.origin.x - screenBounds.origin.x + outset;
if (offset < 0.0f)
{
pixelBounds.origin.x -= offset;
pixelBounds.size.width += offset;
}
// Clip bottom
offset = newPosition.y + pixelBounds.origin.y + pixelBounds.size.height - screenBounds.origin.y - screenBounds.size.height - outset;
if (offset > 0.0f)
{
pixelBounds.size.height -= offset;
}
// Clip right
offset = newPosition.x + pixelBounds.origin.x + pixelBounds.size.width - screenBounds.origin.x - screenBounds.size.width - outset;
if (offset > 0.0f)
{
pixelBounds.size.width -= offset;
}
CGRect clippedBounds = pixelBounds;
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];
[super setPosition:newPosition];
[self addAnimation:positionAnimation forKey:@"animatePosition"];
// bounds are animated non-clipped but set with clipping
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.bounds = clippedBounds;
[self addAnimation:boundsAnimation forKey:@"animateBounds"];
// RMLog(@"new bounds: %f %f %f %f", self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width, self.bounds.size.height);
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(-pixelBounds.origin.x / pixelBounds.size.width, -pixelBounds.origin.y / pixelBounds.size.height);
// anchorPoint is animated non-clipped but set with clipping
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.anchorPoint = clippedAnchorPoint;
[self addAnimation:anchorPointAnimation forKey:@"animateAnchorPoint"];
// scale path
shapeLayer.lineWidth = scaledLineWidth;
// NSLog(@"line width = %f, content scale = %f", scaledLineWidth, [mapView metersPerPixel]);
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;
}
}
CGAffineTransform scaling = CGAffineTransformMakeScale(scale, scale);
UIBezierPath *scaledPath = [bezierPath copy];
[scaledPath applyTransform:scaling];
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.path = scaledPath.CGPath;
[shapeLayer addAnimation:animation forKey:@"animatePath"];
[scaledPath release];
[self setNeedsDisplay];
}
#pragma mark -
- (void)addPointToProjectedPoint:(RMProjectedPoint)point withDrawing:(BOOL)isDrawing
{
// RMLog(@"addLineToXY %f %f", point.x, point.y);
if (isFirstPoint)
{
isFirstPoint = FALSE;
projectedLocation = point;
self.position = [mapView projectedPointToPixel:projectedLocation];
// RMLog(@"screen position set to %f %f", self.position.x, self.position.y);
[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)];
[self recalculateGeometry];
}
[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 *aShape))block
{
ignorePathUpdates = YES;
block(self);
ignorePathUpdates = NO;
[self recalculateGeometry];
}
#pragma mark - Accessors
- (void)closePath
{
[bezierPath closePath];
}
- (float)lineWidth
{
return lineWidth;
}
- (void)setLineWidth:(float)newLineWidth
{
lineWidth = newLineWidth;
[self recalculateGeometry];
}
- (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
{
if (CGPointEqualToPoint(newPosition, super.position) && CGRectEqualToRect(self.bounds, previousBounds))
return;
[self recalculateGeometry];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment