PageRenderTime 70ms CodeModel.GetById 2ms app.highlight 64ms RepoModel.GetById 1ms app.codeStats 0ms

/MapView/Map/RMPath.m

http://github.com/route-me/route-me
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