/MapView/Map/RMPath.m
Objective C | 419 lines | 313 code | 72 blank | 34 comment | 56 complexity | f8331829e1c7c352affbc532f9a047c4 MD5 | raw file
1/// 2// RMPath.m 3// 4// Copyright (c) 2008-2010, Route-Me Contributors 5// All rights reserved. 6// 7// Redistribution and use in source and binary forms, with or without 8// modification, are permitted provided that the following conditions are met: 9// 10// * Redistributions of source code must retain the above copyright notice, this 11// list of conditions and the following disclaimer. 12// * Redistributions in binary form must reproduce the above copyright notice, 13// this list of conditions and the following disclaimer in the documentation 14// and/or other materials provided with the distribution. 15// 16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26// POSSIBILITY OF SUCH DAMAGE. 27 28#import "RMPath.h" 29#import "RMMapView.h" 30#import "RMMapContents.h" 31#import "RMMercatorToScreenProjection.h" 32#import "RMPixel.h" 33#import "RMProjection.h" 34#import "RMNotifications.h" 35 36@interface RMPath () { 37 RMProjectedPoint projectedLocation; 38 39 CGFloat *_lineDashLengths; 40 CGFloat *_scaledLineDashLengths; 41 size_t _lineDashCount; 42 CGFloat lineDashPhase; 43 44 CGMutablePathRef path; 45 46 RMMapContents *mapContents; 47 48 CGRect originalContentsRect; 49 BOOL redrawPending; 50} 51 52- (void)addPointToXY:(RMProjectedPoint) point withDrawing:(BOOL)isDrawing; 53- (void)recalculateGeometry; 54@end 55 56@implementation RMPath 57@synthesize lineCap, lineJoin, lineWidth, lineColor, fillColor, scaleLineWidth, shadowBlur, shadowOffset, shadowColor, lineDashPhase, lineDashLengths, scaleLineDash, projectedLocation, enableDragging, enableRotation; 58@dynamic CGPath, projectedBounds; 59 60- (id) initWithContents: (RMMapContents*)aContents { 61 if (![super init]) return nil; 62 63 mapContents = aContents; 64 65 path = CGPathCreateMutable(); 66 67 // Defaults 68 lineWidth = 4.0; 69 lineCap = kCGLineCapRound; 70 lineJoin = kCGLineJoinRound; 71 scaleLineWidth = NO; 72 enableDragging = YES; 73 enableRotation = YES; 74 self.lineColor = [UIColor blackColor]; 75 scaleLineDash = NO; 76 _lineDashCount = 0; 77 _lineDashLengths = NULL; 78 _scaledLineDashLengths = NULL; 79 lineDashPhase = 0.0; 80 shadowBlur = 0.0; 81 shadowOffset = CGSizeMake(0, 0); 82 self.shadowColor = [UIColor clearColor]; 83 84 self.masksToBounds = YES; 85 86 if ( [self respondsToSelector:@selector(setContentsScale:)] ) self.contentsScale = ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] ? [UIScreen mainScreen].scale : 1.0); 87 88 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resumeExpensiveOperationsNotification:) name:RMResumeExpensiveOperations object:nil]; 89 90 return self; 91} 92 93- (id) initForMap: (RMMapView*)map { 94 return [self initWithContents:[map contents]]; 95} 96 97- (id) initForMap: (RMMapView*)map withCoordinates:(const CLLocationCoordinate2D*)coordinates count:(NSInteger)count { 98 if ( !(self = [self initWithContents:[map contents]]) ) return nil; 99 100 [self moveToLatLong:coordinates[0]]; 101 for ( NSInteger i=1; i<count; i++ ) { 102 [self addLineToLatLong:coordinates[i]]; 103 } 104 105 return self; 106} 107 108-(void) dealloc { 109 [[NSNotificationCenter defaultCenter] removeObserver:self]; 110 CGPathRelease(path); 111 [lineColor release]; 112 [fillColor release]; 113 [shadowColor release]; 114 if ( _lineDashLengths ) free(_lineDashLengths); 115 [super dealloc]; 116} 117 118- (id<CAAction>)actionForKey:(NSString *)key { 119 return nil; 120} 121 122- (void) moveToXY: (RMProjectedPoint) point { 123 [self addPointToXY: point withDrawing: FALSE]; 124} 125 126- (void) moveToScreenPoint: (CGPoint) point { 127 [self moveToXY: [[mapContents mercatorToScreenProjection] projectScreenPointToXY: point]]; 128} 129 130- (void) moveToLatLong: (RMLatLong) point { 131 [self moveToXY:[[mapContents projection] latLongToPoint:point]]; 132} 133 134- (void) addLineToXY: (RMProjectedPoint) point { 135 [self addPointToXY: point withDrawing: TRUE]; 136} 137 138- (void) addLineToScreenPoint: (CGPoint) point { 139 [self addLineToXY: [[mapContents mercatorToScreenProjection] projectScreenPointToXY: point]]; 140} 141 142- (void) addLineToLatLong: (RMLatLong) point{ 143 [self addLineToXY:[[mapContents projection] latLongToPoint:point]]; 144} 145 146- (void) closePath { 147 CGPathCloseSubpath(path); 148} 149 150- (void) setLineWidth: (float) newLineWidth { 151 lineWidth = newLineWidth; 152 [self recalculateGeometry]; 153} 154 155- (NSArray *)lineDashLengths { 156 NSMutableArray *lengths = [NSMutableArray arrayWithCapacity:_lineDashCount]; 157 for(size_t dashIndex=0; dashIndex<_lineDashCount; dashIndex++){ 158 [lengths addObject:(id)[NSNumber numberWithFloat:_lineDashLengths[dashIndex]]]; 159 } 160 return lengths; 161} 162- (void) setLineDashLengths:(NSArray *)lengths { 163 if(_lineDashLengths){ 164 free(_lineDashLengths); 165 _lineDashLengths = NULL; 166 167 } 168 if(_scaledLineDashLengths){ 169 free(_scaledLineDashLengths); 170 _scaledLineDashLengths = NULL; 171 } 172 _lineDashCount = [lengths count]; 173 if(!_lineDashCount){ 174 return; 175 } 176 _lineDashLengths = calloc(_lineDashCount, sizeof(CGFloat)); 177 if(!scaleLineDash){ 178 _scaledLineDashLengths = calloc(_lineDashCount, sizeof(CGFloat)); 179 } 180 181 NSEnumerator *lengthEnumerator = [lengths objectEnumerator]; 182 id lenObj; 183 size_t dashIndex = 0; 184 while ((lenObj = [lengthEnumerator nextObject])) { 185 if([lenObj isKindOfClass: [NSNumber class]]){ 186 _lineDashLengths[dashIndex] = [lenObj floatValue]; 187 } else { 188 _lineDashLengths[dashIndex] = 0.0; 189 } 190 dashIndex++; 191 } 192} 193 194-(void)setLineCap:(CGLineCap)theLineCap { 195 if ( theLineCap != lineCap ) { 196 lineCap = theLineCap; 197 [self setNeedsDisplay]; 198 } 199} 200 201-(void)setLineJoin:(CGLineJoin)theLineJoin { 202 if ( theLineJoin != lineJoin ) { 203 lineJoin = theLineJoin; 204 [self setNeedsDisplay]; 205 } 206} 207 208- (void)setLineColor:(UIColor *)aLineColor { 209 if (lineColor != aLineColor) { 210 [lineColor release]; 211 lineColor = [aLineColor retain]; 212 [self setNeedsDisplay]; 213 } 214} 215 216- (void)setFillColor:(UIColor *)aFillColor { 217 if (fillColor != aFillColor) { 218 [fillColor release]; 219 fillColor = [aFillColor retain]; 220 [self setNeedsDisplay]; 221 } 222} 223 224-(void)setShadowBlur:(CGFloat)theShadowBlur { 225 if ( shadowBlur != theShadowBlur ) { 226 shadowBlur = theShadowBlur; 227 [self setNeedsDisplay]; 228 } 229} 230 231-(void)setShadowOffset:(CGSize)theShadowOffset { 232 if ( !CGSizeEqualToSize(shadowOffset, theShadowOffset) ) { 233 shadowOffset = theShadowOffset; 234 [self setNeedsDisplay]; 235 } 236} 237 238-(void)setShadowColor:(UIColor *)theShadowColor { 239 if ( ![shadowColor isEqual:theShadowColor] ) { 240 [theShadowColor retain]; 241 [shadowColor release]; 242 shadowColor = theShadowColor; 243 [self setNeedsDisplay]; 244 } 245} 246 247- (void)moveBy: (CGSize) delta { 248 if(enableDragging){ 249 [super moveBy:delta]; 250 } 251} 252 253- (void)setPosition:(CGPoint)value { 254 [super setPosition:value]; 255 [self recalculateGeometry]; 256} 257 258- (void)addPointToXY:(RMProjectedPoint) point withDrawing:(BOOL)isDrawing { 259 if ( CGPathIsEmpty(path) ) { 260 projectedLocation = point; 261 self.position = [[mapContents mercatorToScreenProjection] projectXYPoint:projectedLocation]; 262 CGPathMoveToPoint(path, NULL, 0.0f, 0.0f); 263 } else { 264 point.easting = point.easting - projectedLocation.easting; 265 point.northing = point.northing - projectedLocation.northing; 266 if ( isDrawing ) { 267 CGPathAddLineToPoint(path, NULL, point.easting, point.northing); 268 } else { 269 CGPathMoveToPoint(path, NULL, point.easting, point.northing); 270 } 271 272 [self recalculateGeometry]; 273 } 274 [self setNeedsDisplay]; 275} 276 277- (void)recalculateGeometry { 278 RMMercatorToScreenProjection *projection = [mapContents mercatorToScreenProjection]; 279 280 float scaledLineWidth = lineWidth; 281 if ( !scaleLineWidth ) { 282 scaledLineWidth *= [mapContents metersPerPixel]; 283 } 284 285 // Get path dimensions (in mercators relative to projectedLocation; bounding box origin is bottom-left due to flipped coord system) 286 CGRect pathDimensions = CGRectInset(CGPathGetBoundingBox(path), -scaledLineWidth, -scaledLineWidth); 287 288 // Convert bounding box to pixels in Quartz coord space, relative to projectedLocation in Quartz coord space 289 CGRect pixelBounds = RMScaleCGRectAboutPoint(CGRectMake(pathDimensions.origin.x, 290 -pathDimensions.origin.y - pathDimensions.size.height, 291 pathDimensions.size.width, 292 pathDimensions.size.height), 293 1.0f / [projection metersPerPixel], CGPointZero); 294 295 296 // Clip bound rect to screen bounds. 297 // If bounds are not clipped, they won't display when you zoom in too much. 298 CGRect screenBounds = [mapContents screenBounds]; 299 CGPoint myPosition = [projection projectXYPoint: projectedLocation]; 300 CGRect clippedBounds = pixelBounds; 301 CGFloat outset = MAX(screenBounds.size.width, screenBounds.size.height); 302 303 clippedBounds.origin.x += myPosition.x; clippedBounds.origin.y += myPosition.y; 304 clippedBounds = CGRectIntersection(clippedBounds, CGRectInset(screenBounds, -outset, -outset)); 305 clippedBounds.origin.x -= myPosition.x; clippedBounds.origin.y -= myPosition.y; 306 BOOL clipped = !CGRectEqualToRect(clippedBounds, pixelBounds); 307 308 CGRect contentsRect = CGRectZero; 309 if ( pixelBounds.size.height > 0 && pixelBounds.size.width > 0 ) { 310 contentsRect = CGRectMake((clippedBounds.origin.x - pixelBounds.origin.x) / pixelBounds.size.width, 311 (clippedBounds.origin.y - pixelBounds.origin.y) / pixelBounds.size.height, 312 clippedBounds.size.width / pixelBounds.size.width, 313 clippedBounds.size.height / pixelBounds.size.height); 314 315 if ( ![RMMapContents performExpensiveOperations] ) { 316 // While moving, just adjust the contents rect instead of redrawing 317 if ( clippedBounds.size.width > 0 && clippedBounds.size.height > 0 ) { 318 // Select a contents rect that is proportonal to the currently drawn region (which may be a subset of the total path bounds) 319 self.contentsRect = CGRectMake((contentsRect.origin.x - originalContentsRect.origin.x) / originalContentsRect.size.width, 320 (contentsRect.origin.y - originalContentsRect.origin.y) / originalContentsRect.size.height, 321 contentsRect.size.width / originalContentsRect.size.width, 322 contentsRect.size.height / originalContentsRect.size.height); 323 } 324 } else { 325 originalContentsRect = contentsRect; 326 } 327 } 328 329 pixelBounds = clippedBounds; 330 331 if ( pixelBounds.size.width > 0 && pixelBounds.size.height > 0 ) { 332 self.anchorPoint = CGPointMake(-pixelBounds.origin.x / pixelBounds.size.width, -pixelBounds.origin.y / pixelBounds.size.height); 333 } 334 335 CGRect priorBounds = self.bounds; 336 self.bounds = pixelBounds; 337 338 [super setPosition:myPosition]; 339 340 if ( redrawPending || fabs(priorBounds.size.width - pixelBounds.size.width) > 1.0 || fabs(priorBounds.size.height - pixelBounds.size.height) > 1.0 || clipped ) { 341 // Redraw if we changed size, clipped the view, or if we were pending a redraw 342 if ( [RMMapContents performExpensiveOperations] ) { 343 redrawPending = NO; 344 self.contentsRect = CGRectMake(0, 0, 1, 1); 345 [self setNeedsDisplay]; 346 } else { 347 redrawPending = YES; 348 } 349 } 350} 351 352- (void)drawInContext:(CGContextRef)theContext { 353 float scale = 1.0f / [mapContents metersPerPixel]; 354 355 float scaledLineWidth = lineWidth; 356 if ( !scaleLineWidth ) { 357 scaledLineWidth *= [mapContents metersPerPixel]; 358 } 359 360 CGFloat *dashLengths = _lineDashLengths; 361 if(!scaleLineDash && _lineDashLengths) { 362 dashLengths = _scaledLineDashLengths; 363 for(size_t dashIndex=0; dashIndex<_lineDashCount; dashIndex++){ 364 dashLengths[dashIndex] = _lineDashLengths[dashIndex] * scale; 365 } 366 } 367 368 CGContextScaleCTM(theContext, scale, -scale); // Flip vertically, as path is in projected coord space with origin with y axis increasing upwards 369 370 CGContextBeginPath(theContext); 371 CGContextAddPath(theContext, path); 372 373 CGContextSetLineWidth(theContext, scaledLineWidth); 374 CGContextSetLineCap(theContext, lineCap); 375 CGContextSetLineJoin(theContext, lineJoin); 376 if(_lineDashLengths){ 377 CGContextSetLineDash(theContext, lineDashPhase, dashLengths, _lineDashCount); 378 } 379 380 if ( lineColor ) { 381 CGContextSetStrokeColorWithColor(theContext, [lineColor CGColor]); 382 } 383 384 if ( ![shadowColor isEqual:[UIColor clearColor]] ) { 385 CGContextSetShadowWithColor(theContext, shadowOffset, shadowBlur, [shadowColor CGColor]); 386 } 387 388 if ( fillColor ) { 389 CGContextSetFillColorWithColor(theContext, [fillColor CGColor]); 390 } 391 392 CGContextDrawPath(theContext, (lineColor && fillColor ? kCGPathFillStroke : (lineColor ? kCGPathStroke : kCGPathFill))); 393} 394 395- (CGPathRef)CGPath { 396 return path; 397} 398 399- (RMProjectedRect)projectedBounds { 400 float scaledLineWidth = lineWidth; 401 if ( !scaleLineWidth ) { 402 scaledLineWidth *= [mapContents metersPerPixel]; 403 } 404 CGRect regionRect = CGRectInset(CGPathGetBoundingBox(path), -scaledLineWidth, -scaledLineWidth); 405 return RMMakeProjectedRect(regionRect.origin.x + projectedLocation.easting, 406 regionRect.origin.y + projectedLocation.northing, 407 regionRect.size.width, 408 regionRect.size.height); 409} 410 411- (void)resumeExpensiveOperationsNotification:(NSNotification*)notification { 412 if ( redrawPending ) { 413 self.contentsRect = CGRectMake(0, 0, 1, 1); 414 [self recalculateGeometry]; 415 redrawPending = NO; 416 } 417} 418 419@end