PageRenderTime 135ms CodeModel.GetById 15ms app.highlight 112ms RepoModel.GetById 1ms app.codeStats 1ms

/MapView/Map/RMMapView.m

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