/MapView/Map/RMMapView.m
Objective C | 777 lines | 548 code | 145 blank | 84 comment | 159 complexity | 7d1e2b6b132507527424f98cf2470f78 MD5 | raw file
1// 2// RMMapView.m 3// 4// Copyright (c) 2008-2009, 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 "RMMapView.h" 29#import "RMMapContents.h" 30#import "RMMapViewDelegate.h" 31 32#import "RMTileLoader.h" 33 34#import "RMMercatorToScreenProjection.h" 35#import "RMMarker.h" 36#import "RMProjection.h" 37#import "RMMarkerManager.h" 38 39@interface RMMapView (PrivateMethods) 40// methods for post-touch deceleration, ala UIScrollView 41- (void)startDecelerationWithDelta:(CGSize)delta; 42- (void)incrementDeceleration:(NSTimer *)timer; 43- (void)stopDeceleration; 44@end 45 46@implementation RMMapView 47@synthesize contents; 48 49@synthesize decelerationFactor; 50@synthesize deceleration; 51 52@synthesize rotation; 53 54@synthesize enableDragging; 55@synthesize enableZoom; 56@synthesize enableRotate; 57 58#pragma mark --- begin constants ---- 59#define kDefaultDecelerationFactor .88f 60#define kMinDecelerationDelta 0.01f 61#pragma mark --- end constants ---- 62 63- (RMMarkerManager*)markerManager 64{ 65 return self.contents.markerManager; 66} 67 68-(void) performInitialSetup 69{ 70 LogMethod(); 71 72 enableDragging = YES; 73 enableZoom = YES; 74 enableRotate = NO; 75 decelerationFactor = kDefaultDecelerationFactor; 76 deceleration = NO; 77 78 screenScale = 0.0; 79 80 // [self recalculateImageSet]; 81 82 if (enableZoom || enableRotate) 83 [self setMultipleTouchEnabled:TRUE]; 84 85 self.backgroundColor = [UIColor grayColor]; 86 87 _constrainMovement=NO; 88 89// [[NSURLCache sharedURLCache] removeAllCachedResponses]; 90} 91 92- (id)initWithFrame:(CGRect)frame 93{ 94 return [self initWithFrame:frame screenScale:0.0]; 95} 96 97- (id)initWithFrame:(CGRect)frame screenScale:(float)theScreenScale 98{ 99 LogMethod(); 100 if (self = [super initWithFrame:frame]) { 101 [self performInitialSetup]; 102 screenScale = theScreenScale; 103 } 104 return self; 105} 106 107/// \deprecated Deprecated any time after 0.5. 108- (id)initWithFrame:(CGRect)frame WithLocation:(CLLocationCoordinate2D)latlon 109{ 110 WarnDeprecated(); 111 LogMethod(); 112 if (self = [super initWithFrame:frame]) { 113 [self performInitialSetup]; 114 } 115 [self moveToLatLong:latlon]; 116 return self; 117} 118 119//=========================================================== 120// contents 121//=========================================================== 122- (RMMapContents *)contents 123{ 124 if (!_contentsIsSet) { 125 RMMapContents *newContents = [[RMMapContents alloc] initWithView:self screenScale:screenScale]; 126 self.contents = newContents; 127 [newContents release]; 128 _contentsIsSet = YES; 129 } 130 return contents; 131} 132- (void)setContents:(RMMapContents *)theContents 133{ 134 if (contents != theContents) { 135 [contents release]; 136 contents = [theContents retain]; 137 _contentsIsSet = YES; 138 [self performInitialSetup]; 139 } 140} 141 142-(void) dealloc 143{ 144 LogMethod(); 145 self.contents = nil; 146 [super dealloc]; 147} 148 149-(void) drawRect: (CGRect) rect 150{ 151 [self.contents drawRect:rect]; 152} 153 154-(NSString*) description 155{ 156 CGRect bounds = [self bounds]; 157 return [NSString stringWithFormat:@"MapView at %.0f,%.0f-%.0f,%.0f", bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height]; 158} 159 160/// Forward invocations to RMMapContents 161- (void)forwardInvocation:(NSInvocation *)invocation 162{ 163 SEL aSelector = [invocation selector]; 164 165 if ([self.contents respondsToSelector:aSelector]) 166 [invocation invokeWithTarget:self.contents]; 167 else 168 [self doesNotRecognizeSelector:aSelector]; 169} 170 171- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 172{ 173 if ([super respondsToSelector:aSelector]) 174 return [super methodSignatureForSelector:aSelector]; 175 else 176 return [self.contents methodSignatureForSelector:aSelector]; 177} 178 179#pragma mark Delegate 180 181@dynamic delegate; 182 183- (void) setDelegate: (id<RMMapViewDelegate>) _delegate 184{ 185 if (delegate == _delegate) return; 186 delegate = _delegate; 187 188 _delegateHasBeforeMapMove = [(NSObject*) delegate respondsToSelector: @selector(beforeMapMove:)]; 189 _delegateHasAfterMapMove = [(NSObject*) delegate respondsToSelector: @selector(afterMapMove:)]; 190 191 _delegateHasBeforeMapZoomByFactor = [(NSObject*) delegate respondsToSelector: @selector(beforeMapZoom: byFactor: near:)]; 192 _delegateHasAfterMapZoomByFactor = [(NSObject*) delegate respondsToSelector: @selector(afterMapZoom: byFactor: near:)]; 193 194 _delegateHasMapViewRegionDidChange = [delegate respondsToSelector:@selector(mapViewRegionDidChange:)]; 195 196 _delegateHasBeforeMapRotate = [(NSObject*) delegate respondsToSelector: @selector(beforeMapRotate: fromAngle:)]; 197 _delegateHasAfterMapRotate = [(NSObject*) delegate respondsToSelector: @selector(afterMapRotate: toAngle:)]; 198 199 _delegateHasDoubleTapOnMap = [(NSObject*) delegate respondsToSelector: @selector(doubleTapOnMap:At:)]; 200 _delegateHasSingleTapOnMap = [(NSObject*) delegate respondsToSelector: @selector(singleTapOnMap:At:)]; 201 202 _delegateHasTapOnMarker = [(NSObject*) delegate respondsToSelector:@selector(tapOnMarker:onMap:)]; 203 _delegateHasTapOnLabelForMarker = [(NSObject*) delegate respondsToSelector:@selector(tapOnLabelForMarker:onMap:)]; 204 _delegateHasTapOnLabelForMarkerOnLayer = [(NSObject*) delegate respondsToSelector:@selector(tapOnLabelForMarker:onMap:onLayer:)]; 205 206 _delegateHasAfterMapTouch = [(NSObject*) delegate respondsToSelector: @selector(afterMapTouch:)]; 207 208 _delegateHasShouldDragMarker = [(NSObject*) delegate respondsToSelector: @selector(mapView: shouldDragMarker: withEvent:)]; 209 _delegateHasDidDragMarker = [(NSObject*) delegate respondsToSelector: @selector(mapView: didDragMarker: withEvent:)]; 210 211 _delegateHasDragMarkerPosition = [(NSObject*) delegate respondsToSelector: @selector(dragMarkerPosition: onMap: position:)]; 212} 213 214- (id<RMMapViewDelegate>) delegate 215{ 216 return delegate; 217} 218 219#pragma mark Movement 220 221-(void) moveToProjectedPoint: (RMProjectedPoint) aPoint 222{ 223 if (_delegateHasBeforeMapMove) [delegate beforeMapMove: self]; 224 [self.contents moveToProjectedPoint:aPoint]; 225 if (_delegateHasAfterMapMove) [delegate afterMapMove: self]; 226 if (_delegateHasMapViewRegionDidChange) [delegate mapViewRegionDidChange:self]; 227} 228-(void) moveToLatLong: (CLLocationCoordinate2D) point 229{ 230 if (_delegateHasBeforeMapMove) [delegate beforeMapMove: self]; 231 [self.contents moveToLatLong:point]; 232 if (_delegateHasAfterMapMove) [delegate afterMapMove: self]; 233 if (_delegateHasMapViewRegionDidChange) [delegate mapViewRegionDidChange:self]; 234} 235 236-(void)setConstraintsSW:(CLLocationCoordinate2D)sw NE:(CLLocationCoordinate2D)ne 237{ 238 //store projections 239 RMProjection *proj=self.contents.projection; 240 241 RMProjectedPoint projectedNE = [proj latLongToPoint:ne]; 242 RMProjectedPoint projectedSW = [proj latLongToPoint:sw]; 243 244 [self setProjectedContraintsSW:projectedSW NE:projectedNE]; 245} 246 247- (void)setProjectedContraintsSW:(RMProjectedPoint)sw NE:(RMProjectedPoint)ne { 248 SWconstraint = sw; 249 NEconstraint = ne; 250 251 _constrainMovement=YES; 252} 253 254-(void)moveBy:(CGSize)delta 255{ 256 257 if ( _constrainMovement ) 258 { 259 //bounds are 260 RMMercatorToScreenProjection *mtsp=self.contents.mercatorToScreenProjection; 261 262 //calculate new bounds after move 263 RMProjectedRect pBounds=[mtsp projectedBounds]; 264 RMProjectedSize XYDelta = [mtsp projectScreenSizeToXY:delta]; 265 CGSize sizeRatio = CGSizeMake(((delta.width == 0) ? 0 : XYDelta.width / delta.width), 266 ((delta.height == 0) ? 0 : XYDelta.height / delta.height)); 267 RMProjectedRect newBounds=pBounds; 268 269 //move the rect by delta 270 newBounds.origin.northing -= XYDelta.height; 271 newBounds.origin.easting -= XYDelta.width; 272 273 // see if new bounds are within constrained bounds, and constrain if necessary 274 BOOL constrained = NO; 275 if ( newBounds.origin.northing < SWconstraint.northing ) { newBounds.origin.northing = SWconstraint.northing; constrained = YES; } 276 if ( newBounds.origin.northing+newBounds.size.height > NEconstraint.northing ) { newBounds.origin.northing = NEconstraint.northing - newBounds.size.height; constrained = YES; } 277 if ( newBounds.origin.easting < SWconstraint.easting ) { newBounds.origin.easting = SWconstraint.easting; constrained = YES; } 278 if ( newBounds.origin.easting+newBounds.size.width > NEconstraint.easting ) { newBounds.origin.easting = NEconstraint.easting - newBounds.size.width; constrained = YES; } 279 if ( constrained ) 280 { 281 // Adjust delta to match constraint 282 XYDelta.height = pBounds.origin.northing - newBounds.origin.northing; 283 XYDelta.width = pBounds.origin.easting - newBounds.origin.easting; 284 delta = CGSizeMake(((sizeRatio.width == 0) ? 0 : XYDelta.width / sizeRatio.width), 285 ((sizeRatio.height == 0) ? 0 : XYDelta.height / sizeRatio.height)); 286 } 287 } 288 289 if (_delegateHasBeforeMapMove) [delegate beforeMapMove: self]; 290 [self.contents moveBy:delta]; 291 if (_delegateHasAfterMapMove) [delegate afterMapMove: self]; 292 if (_delegateHasMapViewRegionDidChange) [delegate mapViewRegionDidChange:self]; 293} 294 295- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center 296{ 297 [self zoomByFactor:zoomFactor near:center animated:NO]; 298} 299- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center animated:(BOOL)animated 300{ 301 if ( _constrainMovement ) 302 { 303 //check that bounds after zoom don't exceed map constraints 304 //the logic is copued from the method zoomByFactor, 305 float _zoomFactor = [self.contents adjustZoomForBoundingMask:zoomFactor]; 306 float zoomDelta = log2f(_zoomFactor); 307 float targetZoom = zoomDelta + [self.contents zoom]; 308 BOOL canZoom=NO; 309 if (targetZoom == [self.contents zoom]){ 310 //OK... . I could even do a return here.. but it will hamper with future logic.. 311 canZoom=YES; 312 } 313 // clamp zoom to remain below or equal to maxZoom after zoomAfter will be applied 314 if(targetZoom > [self.contents maxZoom]){ 315 zoomFactor = exp2f([self.contents maxZoom] - [self.contents zoom]); 316 } 317 318 // clamp zoom to remain above or equal to minZoom after zoomAfter will be applied 319 if(targetZoom < [self.contents minZoom]){ 320 zoomFactor = 1/exp2f([self.contents zoom] - [self.contents minZoom]); 321 } 322 323 //bools for syntactical sugar to understand the logic in the if statement below 324 BOOL zoomAtMax = ([self.contents zoom] == [self.contents maxZoom]); 325 BOOL zoomAtMin = ([self.contents zoom] == [self.contents minZoom]); 326 BOOL zoomGreaterMin = ([self.contents zoom] > [self.contents minZoom]); 327 BOOL zoomLessMax = ([self.contents zoom] < [ self.contents maxZoom]); 328 329 //zooming in zoomFactor > 1 330 //zooming out zoomFactor < 1 331 332 if ((zoomGreaterMin && zoomLessMax) || (zoomAtMax && zoomFactor<1) || (zoomAtMin && zoomFactor>1)) 333 { 334 //if I'm here it means I could zoom, now we have to see what will happen after zoom 335 RMMercatorToScreenProjection *mtsp= self.contents.mercatorToScreenProjection ; 336 337 //get copies of mercatorRoScreenProjection's data 338 RMProjectedPoint origin=[mtsp origin]; 339 float metersPerPixel=mtsp.metersPerPixel; 340 CGRect screenBounds=[mtsp screenBounds]; 341 342 //tjis is copied from [RMMercatorToScreenBounds zoomScreenByFactor] 343 // First we move the origin to the pivot... 344 origin.easting += center.x * metersPerPixel; 345 origin.northing += (screenBounds.size.height - center.y) * metersPerPixel; 346 // Then scale by 1/factor 347 metersPerPixel /= _zoomFactor; 348 // Then translate back 349 origin.easting -= center.x * metersPerPixel; 350 origin.northing -= (screenBounds.size.height - center.y) * metersPerPixel; 351 352 origin = [mtsp.projection wrapPointHorizontally:origin]; 353 354 //calculate new bounds 355 RMProjectedRect zRect; 356 zRect.origin = origin; 357 zRect.size.width = screenBounds.size.width * metersPerPixel; 358 zRect.size.height = screenBounds.size.height * metersPerPixel; 359 360 //can zoom only if within bounds 361 canZoom= zoomDelta > 0 || !(zRect.origin.northing < SWconstraint.northing || zRect.origin.northing+zRect.size.height> NEconstraint.northing || 362 zRect.origin.easting < SWconstraint.easting || zRect.origin.easting+zRect.size.width > NEconstraint.easting); 363 364 } 365 366 if(!canZoom){ 367 RMLog(@"Zooming will move map out of bounds: no zoom"); 368 return; 369 } 370 371 } 372 373 if (_delegateHasBeforeMapZoomByFactor) [delegate beforeMapZoom: self byFactor: zoomFactor near: center]; 374 [self.contents zoomByFactor:zoomFactor near:center animated:animated withCallback:(animated && (_delegateHasAfterMapZoomByFactor || _delegateHasMapViewRegionDidChange))?self:nil]; 375 if (!animated) 376 { 377 if (_delegateHasAfterMapZoomByFactor) [delegate afterMapZoom: self byFactor: zoomFactor near: center]; 378 if (_delegateHasMapViewRegionDidChange) [delegate mapViewRegionDidChange:self]; 379 } 380} 381 382 383#pragma mark RMMapContentsAnimationCallback methods 384 385- (void)animationFinishedWithZoomFactor:(float)zoomFactor near:(CGPoint)p 386{ 387 if (_delegateHasAfterMapZoomByFactor) 388 [delegate afterMapZoom: self byFactor: zoomFactor near: p]; 389} 390 391- (void)animationStepped { 392 if (_delegateHasMapViewRegionDidChange) [delegate mapViewRegionDidChange:self]; 393} 394 395#pragma mark Event handling 396 397- (RMGestureDetails) gestureDetails: (NSSet*) touches 398{ 399 RMGestureDetails gesture; 400 gesture.center.x = gesture.center.y = 0; 401 gesture.averageDistanceFromCenter = 0; 402 gesture.angle = 0.0; 403 404 int interestingTouches = 0; 405 406 for (UITouch *touch in touches) 407 { 408 if ([touch phase] != UITouchPhaseBegan 409 && [touch phase] != UITouchPhaseMoved 410 && [touch phase] != UITouchPhaseStationary) 411 continue; 412 // RMLog(@"phase = %d", [touch phase]); 413 414 interestingTouches++; 415 416 CGPoint location = [touch locationInView: self]; 417 418 gesture.center.x += location.x; 419 gesture.center.y += location.y; 420 } 421 422 if (interestingTouches == 0) 423 { 424 gesture.center = lastGesture.center; 425 gesture.numTouches = 0; 426 gesture.averageDistanceFromCenter = 0.0f; 427 return gesture; 428 } 429 430 // RMLog(@"interestingTouches = %d", interestingTouches); 431 432 gesture.center.x /= interestingTouches; 433 gesture.center.y /= interestingTouches; 434 435 for (UITouch *touch in touches) 436 { 437 if ([touch phase] != UITouchPhaseBegan 438 && [touch phase] != UITouchPhaseMoved 439 && [touch phase] != UITouchPhaseStationary) 440 continue; 441 442 CGPoint location = [touch locationInView: self]; 443 444 // RMLog(@"For touch at %.0f, %.0f:", location.x, location.y); 445 float dx = location.x - gesture.center.x; 446 float dy = location.y - gesture.center.y; 447 // RMLog(@"delta = %.0f, %.0f distance = %f", dx, dy, sqrtf((dx*dx) + (dy*dy))); 448 gesture.averageDistanceFromCenter += sqrtf((dx*dx) + (dy*dy)); 449 } 450 451 gesture.averageDistanceFromCenter /= interestingTouches; 452 453 gesture.numTouches = interestingTouches; 454 455 if ([touches count] == 2) 456 { 457 CGPoint first = [[[touches allObjects] objectAtIndex:0] locationInView:[self superview]]; 458 CGPoint second = [[[touches allObjects] objectAtIndex:1] locationInView:[self superview]]; 459 CGFloat height = second.y - first.y; 460 CGFloat width = first.x - second.x; 461 gesture.angle = atan2(height,width); 462 } 463 464 //RMLog(@"center = %.0f,%.0f dist = %f, angle = %f", gesture.center.x, gesture.center.y, gesture.averageDistanceFromCenter, gesture.angle); 465 466 return gesture; 467} 468 469- (void)resumeExpensiveOperations 470{ 471 [RMMapContents setPerformExpensiveOperations:YES]; 472} 473 474- (void)delayedResumeExpensiveOperations 475{ 476 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(resumeExpensiveOperations) object:nil]; 477 [self performSelector:@selector(resumeExpensiveOperations) withObject:nil afterDelay:0.4]; 478} 479 480- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 481{ 482 UITouch *touch = [[touches allObjects] objectAtIndex:0]; 483 //Check if the touch hit a RMMarker subclass and if so, forward the touch event on 484 //so it can be handled there 485 id furthestLayerDown = [self.contents.overlay hitTest:[touch locationInView:self]]; 486 if ([[furthestLayerDown class]isSubclassOfClass: [RMMarker class]]) { 487 if ([furthestLayerDown respondsToSelector:@selector(touchesBegan:withEvent:)]) { 488 [furthestLayerDown performSelector:@selector(touchesBegan:withEvent:) withObject:touches withObject:event]; 489 return; 490 } 491 } 492 493 if (lastGesture.numTouches == 0) 494 { 495 [RMMapContents setPerformExpensiveOperations:NO]; 496 } 497 498 // RMLog(@"touchesBegan %d", [[event allTouches] count]); 499 lastGesture = [self gestureDetails:[event allTouches]]; 500 501 if(deceleration) 502 { 503 if (_decelerationTimer != nil) { 504 [self stopDeceleration]; 505 } 506 } 507 508 [self delayedResumeExpensiveOperations]; 509} 510 511/// \bug touchesCancelled should clean up, not pass event to markers 512- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 513{ 514 UITouch *touch = [[touches allObjects] objectAtIndex:0]; 515 516 //Check if the touch hit a RMMarker subclass and if so, forward the touch event on 517 //so it can be handled there 518 id furthestLayerDown = [self.contents.overlay hitTest:[touch locationInView:self]]; 519 if ([[furthestLayerDown class]isSubclassOfClass: [RMMarker class]]) { 520 if ([furthestLayerDown respondsToSelector:@selector(touchesCancelled:withEvent:)]) { 521 [furthestLayerDown performSelector:@selector(touchesCancelled:withEvent:) withObject:touches withObject:event]; 522 return; 523 } 524 } 525 526 [self delayedResumeExpensiveOperations]; 527} 528 529- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 530{ 531 UITouch *touch = [[touches allObjects] objectAtIndex:0]; 532 533 //Check if the touch hit a RMMarker subclass and if so, forward the touch event on 534 //so it can be handled there 535 id furthestLayerDown = [self.contents.overlay hitTest:[touch locationInView:self]]; 536 if ([[furthestLayerDown class]isSubclassOfClass: [RMMarker class]]) { 537 if ([furthestLayerDown respondsToSelector:@selector(touchesEnded:withEvent:)]) { 538 [furthestLayerDown performSelector:@selector(touchesEnded:withEvent:) withObject:touches withObject:event]; 539 return; 540 } 541 } 542 NSInteger lastTouches = lastGesture.numTouches; 543 544 // Calculate the gesture. 545 lastGesture = [self gestureDetails:[event allTouches]]; 546 547 BOOL decelerating = NO; 548 if (touch.tapCount >= 2) 549 { 550 if (_delegateHasDoubleTapOnMap) { 551 [delegate doubleTapOnMap: self At: lastGesture.center]; 552 } else { 553 // Default behaviour matches built in maps.app 554 float nextZoomFactor = [self.contents nextNativeZoomFactor]; 555 if (nextZoomFactor != 0) 556 [self zoomByFactor:nextZoomFactor near:[touch locationInView:self] animated:YES]; 557 } 558 } else if (lastTouches == 1 && touch.tapCount != 1) { 559 // deceleration 560 if(deceleration && enableDragging) 561 { 562 CGPoint prevLocation = [touch previousLocationInView:self]; 563 CGPoint currLocation = [touch locationInView:self]; 564 CGSize touchDelta = CGSizeMake(currLocation.x - prevLocation.x, currLocation.y - prevLocation.y); 565 [self startDecelerationWithDelta:touchDelta]; 566 decelerating = YES; 567 } 568 } 569 570 571 // If there are no more fingers on the screen, resume any slow operations. 572 if (lastGesture.numTouches == 0 && !decelerating) 573 { 574 [self delayedResumeExpensiveOperations]; 575 } 576 577 578 if (touch.tapCount == 1) 579 { 580 if(lastGesture.numTouches == 0) 581 { 582 CALayer* hit = [self.contents.overlay hitTest:[touch locationInView:self]]; 583 // RMLog(@"LAYER of type %@",[hit description]); 584 585 if (hit != nil) { 586 CALayer *superlayer = [hit superlayer]; 587 588 // See if tap was on a marker or marker label and send delegate protocol method 589 if ([hit isKindOfClass: [RMMarker class]]) { 590 if (_delegateHasTapOnMarker) { 591 [delegate tapOnMarker:(RMMarker*)hit onMap:self]; 592 } 593 } else if (superlayer != nil && [superlayer isKindOfClass: [RMMarker class]]) { 594 if (_delegateHasTapOnLabelForMarker) { 595 [delegate tapOnLabelForMarker:(RMMarker*)superlayer onMap:self]; 596 } 597 if (_delegateHasTapOnLabelForMarkerOnLayer) { 598 [delegate tapOnLabelForMarker:(RMMarker*)superlayer onMap:self onLayer:hit]; 599 } 600 } else if ([superlayer superlayer] != nil && [[superlayer superlayer] isKindOfClass: [RMMarker class]]) { 601 if (_delegateHasTapOnLabelForMarker) { 602 [delegate tapOnLabelForMarker:(RMMarker*)[superlayer superlayer] onMap:self]; 603 } 604 if (_delegateHasTapOnLabelForMarkerOnLayer) { 605 [delegate tapOnLabelForMarker:(RMMarker*)[superlayer superlayer] onMap:self onLayer:hit]; 606 } 607 } else if (_delegateHasSingleTapOnMap) { 608 [delegate singleTapOnMap: self At: [touch locationInView:self]]; 609 } 610 } 611 } 612 else if(!enableDragging && (lastGesture.numTouches == 1)) 613 { 614 float prevZoomFactor = [self.contents prevNativeZoomFactor]; 615 if (prevZoomFactor != 0) 616 [self zoomByFactor:prevZoomFactor near:[touch locationInView:self] animated:YES]; 617 } 618 } 619 620 if (_delegateHasAfterMapTouch) [delegate afterMapTouch: self]; 621} 622 623- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 624{ 625 UITouch *touch = [[touches allObjects] objectAtIndex:0]; 626 627 //Check if the touch hit a RMMarker subclass and if so, forward the touch event on 628 //so it can be handled there 629 id furthestLayerDown = [self.contents.overlay hitTest:[touch locationInView:self]]; 630 if ([[furthestLayerDown class]isSubclassOfClass: [RMMarker class]]) { 631 if ([furthestLayerDown respondsToSelector:@selector(touchesMoved:withEvent:)]) { 632 [furthestLayerDown performSelector:@selector(touchesMoved:withEvent:) withObject:touches withObject:event]; 633 return; 634 } 635 } 636 637 CALayer* hit = [self.contents.overlay hitTest:[touch locationInView:self]]; 638// RMLog(@"LAYER of type %@",[hit description]); 639 640 if (hit != nil) { 641 642 if ([hit isKindOfClass: [RMMarker class]]) { 643 644 if (!_delegateHasShouldDragMarker || (_delegateHasShouldDragMarker && [delegate mapView:self shouldDragMarker:(RMMarker*)hit withEvent:event])) { 645 if (_delegateHasDidDragMarker) { 646 [delegate mapView:self didDragMarker:(RMMarker*)hit withEvent:event]; 647 return; 648 } 649 } 650 } 651 } 652 653 RMGestureDetails newGesture = [self gestureDetails:[event allTouches]]; 654 655 if(enableRotate && (newGesture.numTouches == lastGesture.numTouches)) 656 { 657 if(newGesture.numTouches == 2) 658 { 659 CGFloat angleDiff = lastGesture.angle - newGesture.angle; 660 CGFloat newAngle = self.rotation + angleDiff; 661 662 [self setRotation:newAngle]; 663 } 664 } 665 666 if (enableDragging && newGesture.numTouches == lastGesture.numTouches) 667 { 668 CGSize delta; 669 delta.width = newGesture.center.x - lastGesture.center.x; 670 delta.height = newGesture.center.y - lastGesture.center.y; 671 672 if (enableZoom && newGesture.numTouches > 1) 673 { 674 NSAssert (lastGesture.averageDistanceFromCenter > 0.0f && newGesture.averageDistanceFromCenter > 0.0f, 675 @"Distance from center is zero despite >1 touches on the screen"); 676 677 double zoomFactor = newGesture.averageDistanceFromCenter / lastGesture.averageDistanceFromCenter; 678 679 [self moveBy:delta]; 680 [self zoomByFactor: zoomFactor near: newGesture.center]; 681 } 682 else 683 { 684 [self moveBy:delta]; 685 } 686 } 687 688 lastGesture = newGesture; 689 690 [self delayedResumeExpensiveOperations]; 691} 692 693// first responder needed to use UIMenuController 694- (BOOL)canBecomeFirstResponder 695{ 696 return YES; 697} 698 699#pragma mark Deceleration 700 701- (void)startDecelerationWithDelta:(CGSize)delta { 702 if (ABS(delta.width) >= 1.0f && ABS(delta.height) >= 1.0f) { 703 _decelerationDelta = delta; 704 if ( !_decelerationTimer ) { 705 _decelerationTimer = [NSTimer scheduledTimerWithTimeInterval:0.01f 706 target:self 707 selector:@selector(incrementDeceleration:) 708 userInfo:nil 709 repeats:YES]; 710 } 711 } 712} 713 714- (void)incrementDeceleration:(NSTimer *)timer { 715 if (ABS(_decelerationDelta.width) < kMinDecelerationDelta && ABS(_decelerationDelta.height) < kMinDecelerationDelta) { 716 [self stopDeceleration]; 717 718 // Resume any slow operations after deceleration completes 719 [self delayedResumeExpensiveOperations]; 720 721 return; 722 } 723 724 // avoid calling delegate methods? design call here 725 [self.contents moveBy:_decelerationDelta]; 726 727 _decelerationDelta.width *= [self decelerationFactor]; 728 _decelerationDelta.height *= [self decelerationFactor]; 729} 730 731- (void)stopDeceleration { 732 if (_decelerationTimer != nil) { 733 [_decelerationTimer invalidate]; 734 _decelerationTimer = nil; 735 _decelerationDelta = CGSizeZero; 736 737 // call delegate methods; design call (see above) 738 [self moveBy:CGSizeZero]; 739 } 740} 741 742/// Must be called by higher didReceiveMemoryWarning 743- (void)didReceiveMemoryWarning 744{ 745 LogMethod(); 746 [contents didReceiveMemoryWarning]; 747} 748 749- (void)setFrame:(CGRect)frame 750{ 751 CGRect r = self.frame; 752 [super setFrame:frame]; 753 // only change if the frame changes AND there is contents 754 if (!CGRectEqualToRect(r, frame) && contents) { 755 [contents setFrame:frame]; 756 } 757} 758 759- (void)setRotation:(CGFloat)angle 760{ 761 if (_delegateHasBeforeMapRotate) [delegate beforeMapRotate: self fromAngle: rotation]; 762 763 [CATransaction begin]; 764 [CATransaction setValue:[NSNumber numberWithFloat:0.0f] forKey:kCATransactionAnimationDuration]; 765 [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; 766 767 rotation = angle; 768 769 self.transform = CGAffineTransformMakeRotation(rotation); 770 [contents setRotation:rotation]; 771 772 [CATransaction commit]; 773 774 if (_delegateHasAfterMapRotate) [delegate afterMapRotate: self toAngle: rotation]; 775} 776 777@end