PageRenderTime 286ms CodeModel.GetById 160ms app.highlight 118ms RepoModel.GetById 1ms app.codeStats 0ms

/MapView/Map/RMMapContents.m

http://github.com/route-me/route-me
Objective C | 1120 lines | 817 code | 214 blank | 89 comment | 88 complexity | 8d1de5303be5f1482eacc42332b7b3a2 MD5 | raw file
   1//
   2//  RMMapContents.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#import "RMGlobalConstants.h"
  28#import "RMMapContents.h"
  29
  30#import "RMMapView.h"
  31
  32#import "RMFoundation.h"
  33#import "RMProjection.h"
  34#import "RMMercatorToScreenProjection.h"
  35#import "RMMercatorToTileProjection.h"
  36
  37#import "RMTileSource.h"
  38#import "RMTileLoader.h"
  39#import "RMTileImageSet.h"
  40
  41#import "RMOpenStreetMapSource.h"
  42#import "RMCoreAnimationRenderer.h"
  43#import "RMCachedTileSource.h"
  44
  45#import "RMLayerCollection.h"
  46#import "RMMarkerManager.h"
  47
  48#import "RMMarker.h"
  49
  50
  51
  52
  53@interface RMMapContents (PrivateMethods)
  54- (void)animatedZoomStep:(NSTimer *)timer;
  55@end
  56
  57@implementation RMMapContents (Internal)
  58	BOOL delegateHasRegionUpdate;
  59@end
  60
  61@implementation RMMapContents
  62
  63@synthesize boundingMask;
  64@synthesize minZoom;
  65@synthesize maxZoom;
  66@synthesize screenScale;
  67@synthesize markerManager;
  68
  69#pragma mark --- begin constants ----
  70#define kZoomAnimationStepTime 0.03f
  71#define kZoomAnimationAnimationTime 0.1f
  72#define kiPhoneMilimeteresPerPixel .1543
  73#define kZoomRectPixelBuffer 50
  74#pragma mark --- end constants ----
  75
  76#pragma mark Initialisation
  77
  78- (id)initWithView: (UIView*) view
  79{	
  80	LogMethod();
  81	CLLocationCoordinate2D here;
  82	here.latitude = kDefaultInitialLatitude;
  83	here.longitude = kDefaultInitialLongitude;
  84	
  85	return [self initWithView:view
  86				   tilesource:[[RMOpenStreetMapSource alloc] init]
  87				 centerLatLon:here
  88	  			    zoomLevel:kDefaultInitialZoomLevel
  89				 maxZoomLevel:kDefaultMaximumZoomLevel
  90				 minZoomLevel:kDefaultMinimumZoomLevel
  91			  backgroundImage:nil
  92                  screenScale:0];
  93}
  94
  95- (id)initWithView: (UIView*) view screenScale:(float)theScreenScale {
  96    LogMethod();
  97	CLLocationCoordinate2D here;
  98	here.latitude = kDefaultInitialLatitude;
  99	here.longitude = kDefaultInitialLongitude;
 100	
 101	return [self initWithView:view
 102				   tilesource:[[RMOpenStreetMapSource alloc] init]
 103				 centerLatLon:here
 104	  			    zoomLevel:kDefaultInitialZoomLevel
 105				 maxZoomLevel:kDefaultMaximumZoomLevel
 106				 minZoomLevel:kDefaultMinimumZoomLevel
 107			  backgroundImage:nil
 108                  screenScale:theScreenScale];
 109}
 110
 111- (id)initWithView: (UIView*) view
 112		tilesource:(id<RMTileSource>)newTilesource
 113{
 114    return [self initWithView:view tilesource:newTilesource screenScale:0.0];
 115}
 116
 117-(id)initWithView:(UIView *)view tilesource:(id<RMTileSource>)newTilesource screenScale:(float)theScreenScale
 118{	
 119	LogMethod();
 120	CLLocationCoordinate2D here;
 121	here.latitude = kDefaultInitialLatitude;
 122	here.longitude = kDefaultInitialLongitude;
 123	
 124	return [self initWithView:view
 125				   tilesource:newTilesource
 126				 centerLatLon:here
 127					zoomLevel:kDefaultInitialZoomLevel
 128				 maxZoomLevel:kDefaultMaximumZoomLevel
 129				 minZoomLevel:kDefaultMinimumZoomLevel
 130			  backgroundImage:nil
 131                  screenScale:theScreenScale];
 132}
 133
 134- (id)initWithView:(UIView*)newView
 135		tilesource:(id<RMTileSource>)newTilesource
 136	  centerLatLon:(CLLocationCoordinate2D)initialCenter
 137		 zoomLevel:(float)initialZoomLevel
 138	  maxZoomLevel:(float)maxZoomLevel
 139	  minZoomLevel:(float)minZoomLevel
 140   backgroundImage:(UIImage *)backgroundImage
 141       screenScale:(float)theScreenScale
 142{
 143	LogMethod();
 144	if (![super init])
 145		return nil;
 146
 147	NSAssert1([newView isKindOfClass:[RMMapView class]], @"view %@ must be a subclass of RMMapView", newView);
 148	[(RMMapView *)newView setContents:self];
 149
 150	tileSource = nil;
 151	projection = nil;
 152	mercatorToTileProjection = nil;
 153	renderer = nil;
 154	imagesOnScreen = nil;
 155	tileLoader = nil;
 156    
 157    screenScale = (theScreenScale == 0.0 ? 1.0 : theScreenScale);
 158
 159	boundingMask = RMMapMinWidthBound;
 160
 161	mercatorToScreenProjection = [[RMMercatorToScreenProjection alloc] initFromProjection:[newTilesource projection] ToScreenBounds:[newView bounds]];
 162	
 163	layer = [[newView layer] retain];
 164
 165        [self setMinZoom:minZoomLevel];
 166        [self setMaxZoom:maxZoomLevel];
 167
 168	[self setTileSource:newTilesource];
 169	[self setRenderer: [[[RMCoreAnimationRenderer alloc] initWithContent:self] autorelease]];
 170	
 171	imagesOnScreen = [[RMTileImageSet alloc] initWithDelegate:renderer];
 172	[imagesOnScreen setTileSource:tileSource];
 173
 174	tileLoader = [[RMTileLoader alloc] initWithContent:self];
 175	[tileLoader setSuppressLoading:YES];
 176
 177	[self setZoom:initialZoomLevel];
 178
 179	[self moveToLatLong:initialCenter];
 180	
 181	[tileLoader setSuppressLoading:NO];
 182	
 183	/// \bug TODO: Make a nice background class
 184	RMMapLayer *theBackground = [[RMMapLayer alloc] init];
 185	[self setBackground:theBackground];
 186	[theBackground release];
 187	
 188	RMLayerCollection *theOverlay = [[RMLayerCollection alloc] initForContents:self];
 189	[self setOverlay:theOverlay];
 190	[theOverlay release];
 191	
 192	markerManager = [[RMMarkerManager alloc] initWithContents:self];
 193	
 194	[newView setNeedsDisplay];
 195	[[NSNotificationCenter defaultCenter] addObserver:self 
 196											 selector:@selector(handleMemoryWarningNotification:) 
 197												 name:UIApplicationDidReceiveMemoryWarningNotification 
 198											   object:nil];
 199
 200	
 201	RMLog(@"Map contents initialised. view: %@ tileSource %@ renderer %@", newView, tileSource, renderer);
 202	return self;
 203}
 204
 205
 206/// deprecated at any moment after release 0.5	
 207- (id) initForView: (UIView*) view
 208{
 209	WarnDeprecated();
 210	return [self initWithView:view];
 211}
 212
 213/// deprecated at any moment after release 0.5
 214- (id) initForView: (UIView*) view WithLocation:(CLLocationCoordinate2D)latlong
 215{
 216	WarnDeprecated();
 217	LogMethod();
 218	id<RMTileSource> _tileSource = [[RMOpenStreetMapSource alloc] init];
 219	RMMapRenderer *_renderer = [[RMCoreAnimationRenderer alloc] initWithContent:self];
 220	
 221	id mapContents = [self initForView:view WithTileSource:_tileSource WithRenderer:_renderer LookingAt:latlong];
 222	[_tileSource release];
 223	[_renderer release];
 224	
 225	return mapContents;
 226}
 227
 228
 229/// deprecated at any moment after release 0.5
 230- (id) initForView: (UIView*) view WithTileSource: (id<RMTileSource>)_tileSource WithRenderer: (RMMapRenderer*)_renderer LookingAt:(CLLocationCoordinate2D)latlong
 231{
 232	WarnDeprecated();
 233	LogMethod();
 234	if (![super init])
 235		return nil;
 236	
 237	NSAssert1([view isKindOfClass:[RMMapView class]], @"view %@ must be a subclass of RMMapView", view);
 238	
 239	self.boundingMask = RMMapMinWidthBound;
 240//	targetView = view;
 241	mercatorToScreenProjection = [[RMMercatorToScreenProjection alloc] initFromProjection:[_tileSource projection] ToScreenBounds:[view bounds]];
 242
 243	tileSource = nil;
 244	projection = nil;
 245	mercatorToTileProjection = nil;
 246	
 247	renderer = nil;
 248	imagesOnScreen = nil;
 249	tileLoader = nil;
 250	
 251	layer = [[view layer] retain];
 252	
 253	[self setTileSource:_tileSource];
 254	[self setRenderer:_renderer];
 255	
 256	imagesOnScreen = [[RMTileImageSet alloc] initWithDelegate:renderer];
 257	[imagesOnScreen setTileSource:tileSource];
 258
 259	tileLoader = [[RMTileLoader alloc] initWithContent:self];
 260	[tileLoader setSuppressLoading:YES];
 261	
 262	[self setMinZoom:kDefaultMinimumZoomLevel];
 263	[self setMaxZoom:kDefaultMaximumZoomLevel];
 264	[self setZoom:kDefaultInitialZoomLevel];
 265
 266	[self moveToLatLong:latlong];
 267	
 268	[tileLoader setSuppressLoading:NO];
 269	
 270	/// \bug TODO: Make a nice background class
 271	RMMapLayer *theBackground = [[RMMapLayer alloc] init];
 272	[self setBackground:theBackground];
 273	[theBackground release];
 274	
 275	RMLayerCollection *theOverlay = [[RMLayerCollection alloc] initForContents:self];
 276	[self setOverlay:theOverlay];
 277	[theOverlay release];
 278	
 279	markerManager = [[RMMarkerManager alloc] initWithContents:self];
 280	
 281	[view setNeedsDisplay];
 282	[[NSNotificationCenter defaultCenter] addObserver:self 
 283											 selector:@selector(handleMemoryWarningNotification:) 
 284												 name:UIApplicationDidReceiveMemoryWarningNotification 
 285											   object:nil];
 286	
 287	RMLog(@"Map contents initialised. view: %@ tileSource %@ renderer %@", view, tileSource, renderer);
 288	
 289	return self;
 290}
 291
 292- (void)setFrame:(CGRect)frame
 293{
 294  CGRect bounds = CGRectMake(0, 0, frame.size.width, frame.size.height);
 295  [mercatorToScreenProjection setScreenBounds:bounds];
 296  background.frame = bounds;
 297  layer.frame = frame;
 298  overlay.frame = bounds;
 299  [tileLoader clearLoadedBounds];
 300  [tileLoader updateLoadedImages];
 301  [renderer setFrame:frame];
 302  [overlay correctPositionOfAllSublayers];
 303}
 304
 305-(void) dealloc
 306{
 307	LogMethod();
 308	[[NSNotificationCenter defaultCenter] removeObserver:self];
 309	[imagesOnScreen cancelLoading];
 310	[self setRenderer:nil];
 311	[imagesOnScreen release];
 312	[tileLoader release];
 313	[projection release];
 314	[mercatorToTileProjection release];
 315	[mercatorToScreenProjection release];
 316	[tileSource release];
 317	[self setOverlay:nil];
 318	[self setBackground:nil];
 319	[layer release];
 320	[markerManager release];
 321	[super dealloc];
 322}
 323
 324- (void)handleMemoryWarningNotification:(NSNotification *)notification
 325{
 326	[self didReceiveMemoryWarning];
 327}
 328
 329- (void) didReceiveMemoryWarning
 330{
 331	LogMethod();
 332	[tileSource didReceiveMemoryWarning];
 333}
 334
 335
 336#pragma mark Forwarded Events
 337
 338- (void)moveToLatLong: (CLLocationCoordinate2D)latlong
 339{
 340	RMProjectedPoint aPoint = [[self projection] latLongToPoint:latlong];
 341	[self moveToProjectedPoint: aPoint];
 342}
 343- (void)moveToProjectedPoint: (RMProjectedPoint)aPoint
 344{
 345	self.centerProjectedPoint = aPoint;
 346}
 347
 348- (void)moveBy: (CGSize) delta
 349{
 350	[mercatorToScreenProjection moveScreenBy:delta];
 351	[imagesOnScreen moveBy:delta];
 352	[tileLoader moveBy:delta];
 353	[overlay moveBy:delta];
 354	[overlay correctPositionOfAllSublayers];
 355	[renderer setNeedsDisplay];
 356}
 357
 358/// \bug doesn't really adjust anything, just makes a computation. CLANG flags some dead assignments (write-only variables)
 359- (float)adjustZoomForBoundingMask:(float)zoomFactor
 360{
 361	if ( boundingMask ==  RMMapNoMinBound )
 362		return zoomFactor;
 363	
 364	double newMPP = self.metersPerPixel / zoomFactor;
 365	
 366	RMProjectedRect mercatorBounds = [[tileSource projection] planetBounds];
 367	
 368	// Check for MinWidthBound
 369	if ( boundingMask & RMMapMinWidthBound )
 370	{
 371		double newMapContentsWidth = mercatorBounds.size.width / newMPP;
 372		double screenBoundsWidth = [self screenBounds].size.width;
 373		double mapContentWidth;
 374		
 375		if ( newMapContentsWidth < screenBoundsWidth )
 376		{
 377			// Calculate new zoom facter so that it does not shrink the map any further. 
 378			mapContentWidth = mercatorBounds.size.width / self.metersPerPixel;
 379			zoomFactor = screenBoundsWidth / mapContentWidth;
 380			
 381			//newMPP = self.metersPerPixel / zoomFactor;
 382			//newMapContentsWidth = mercatorBounds.size.width / newMPP;
 383		}
 384		
 385	}
 386	
 387	// Check for MinHeightBound	
 388	if ( boundingMask & RMMapMinHeightBound )
 389	{
 390		double newMapContentsHeight = mercatorBounds.size.height / newMPP;
 391		double screenBoundsHeight = [self screenBounds].size.height;
 392		double mapContentHeight;
 393		
 394		if ( newMapContentsHeight < screenBoundsHeight )
 395		{
 396			// Calculate new zoom facter so that it does not shrink the map any further. 
 397			mapContentHeight = mercatorBounds.size.height / self.metersPerPixel;
 398			zoomFactor = screenBoundsHeight / mapContentHeight;
 399			
 400			//newMPP = self.metersPerPixel / zoomFactor;
 401			//newMapContentsHeight = mercatorBounds.size.height / newMPP;
 402		}
 403		
 404	}
 405	
 406	//[self adjustMapPlacementWithScale:newMPP];
 407	
 408	return zoomFactor;
 409}
 410
 411/// This currently is not called because it does not handle the case when the map is continous or not continous.  At a certain scale
 412/// you can continuously move to the west or east until you get to a certain scale level that simply shows the entire world.
 413- (void)adjustMapPlacementWithScale:(float)aScale
 414{
 415	CGSize		adjustmentDelta = {0.0, 0.0};
 416	RMLatLong	rightEdgeLatLong = {0, kMaxLong};
 417	RMLatLong	leftEdgeLatLong = {0,- kMaxLong};
 418	
 419	CGPoint		rightEdge = [self latLongToPixel:rightEdgeLatLong withMetersPerPixel:aScale];
 420	CGPoint		leftEdge = [self latLongToPixel:leftEdgeLatLong withMetersPerPixel:aScale];
 421	//CGPoint		topEdge = [self latLongToPixel:myLatLong withMetersPerPixel:aScale];
 422	//CGPoint		bottomEdge = [self latLongToPixel:myLatLong withMetersPerPixel:aScale];
 423	
 424	CGRect		containerBounds = [self screenBounds];
 425
 426	if ( rightEdge.x < containerBounds.size.width ) 
 427	{
 428		adjustmentDelta.width = containerBounds.size.width - rightEdge.x;
 429		[self moveBy:adjustmentDelta];
 430	}
 431	
 432	if ( leftEdge.x > containerBounds.origin.x ) 
 433	{
 434		adjustmentDelta.width = containerBounds.origin.x - leftEdge.x;
 435		[self moveBy:adjustmentDelta];
 436	}
 437	
 438	
 439}
 440
 441/// \bug this is a no-op, not a clamp, if new zoom would be outside of minzoom/maxzoom range
 442- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) pivot
 443{
 444	//[self zoomByFactor:zoomFactor near:pivot animated:NO];
 445	
 446	zoomFactor = [self adjustZoomForBoundingMask:zoomFactor];
 447	//RMLog(@"Zoom Factor: %lf for Zoom:%f", zoomFactor, [self zoom]);
 448	
 449	// pre-calculate zoom so we can tell if we want to perform it
 450	float newZoom = [mercatorToTileProjection  
 451					 calculateZoomFromScale:self.metersPerPixel/zoomFactor];
 452	
 453	if ((newZoom > minZoom) && (newZoom < maxZoom))
 454	{
 455		[mercatorToScreenProjection zoomScreenByFactor:zoomFactor near:pivot];
 456		[imagesOnScreen zoomByFactor:zoomFactor near:pivot];
 457		[tileLoader zoomByFactor:zoomFactor near:pivot];
 458		[overlay zoomByFactor:zoomFactor near:pivot];
 459        [overlay correctPositionOfAllSublayers];
 460		[renderer setNeedsDisplay];
 461	} 
 462}
 463
 464
 465- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) pivot animated:(BOOL) animated
 466{
 467	[self zoomByFactor:zoomFactor near:pivot animated:animated withCallback:nil];
 468}
 469
 470- (BOOL)shouldZoomToTargetZoom:(float)targetZoom withZoomFactor:(float)zoomFactor {
 471	//bools for syntactical sugar to understand the logic in the if statement below
 472	BOOL zoomAtMax = ([self zoom] == [self maxZoom]);
 473	BOOL zoomAtMin = ([self zoom] == [self minZoom]);
 474	BOOL zoomGreaterMin = ([self zoom] > [self minZoom]);
 475	BOOL zoomLessMax = ([self zoom] < [self maxZoom]);
 476	
 477	//zooming in zoomFactor > 1
 478	//zooming out zoomFactor < 1
 479	
 480	if ((zoomGreaterMin && zoomLessMax) || (zoomAtMax && zoomFactor<1) || (zoomAtMin && zoomFactor>1))
 481	{
 482		return YES;
 483	}
 484	else
 485	{
 486		return NO;
 487	}
 488}
 489
 490- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) pivot animated:(BOOL) animated withCallback:(id<RMMapContentsAnimationCallback>)callback
 491{
 492	zoomFactor = [self adjustZoomForBoundingMask:zoomFactor];
 493	float zoomDelta = log2f(zoomFactor);
 494	float targetZoom = zoomDelta + [self zoom];
 495	
 496	if (targetZoom == [self zoom]){
 497		return;
 498	}
 499	// clamp zoom to remain below or equal to maxZoom after zoomAfter will be applied
 500	// Set targetZoom to maxZoom so the map zooms to its maximum
 501	if(targetZoom > [self maxZoom]){
 502		zoomFactor = exp2f([self maxZoom] - [self zoom]);
 503		targetZoom = [self maxZoom];
 504	}
 505	
 506	// clamp zoom to remain above or equal to minZoom after zoomAfter will be applied
 507	// Set targetZoom to minZoom so the map zooms to its maximum
 508	if(targetZoom < [self minZoom]){
 509		zoomFactor = 1/exp2f([self zoom] - [self minZoom]);
 510		targetZoom = [self minZoom];
 511	}
 512
 513    if ([self shouldZoomToTargetZoom:targetZoom withZoomFactor:zoomFactor])
 514    {
 515        if (animated)
 516        {
 517            // goal is to complete the animation in animTime seconds
 518            double nSteps = round(kZoomAnimationAnimationTime / kZoomAnimationStepTime);
 519            double zoomIncr = zoomDelta / nSteps;
 520            
 521            NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
 522                                      [NSNumber numberWithDouble:zoomIncr], @"zoomIncr",
 523                                      [NSNumber numberWithDouble:targetZoom], @"targetZoom",
 524                                      [NSValue valueWithCGPoint:pivot], @"pivot",
 525                                      [NSNumber numberWithFloat:zoomFactor], @"factor",
 526                                      callback, @"callback", nil];
 527            [NSTimer scheduledTimerWithTimeInterval:kZoomAnimationStepTime
 528                                             target:self
 529                                           selector:@selector(animatedZoomStep:)
 530                                           userInfo:userInfo
 531                                            repeats:YES];
 532        }
 533        else
 534        {
 535            [mercatorToScreenProjection zoomScreenByFactor:zoomFactor near:pivot];
 536            [imagesOnScreen zoomByFactor:zoomFactor near:pivot];
 537            [tileLoader zoomByFactor:zoomFactor near:pivot];
 538            [overlay zoomByFactor:zoomFactor near:pivot];
 539            [overlay correctPositionOfAllSublayers];
 540            [renderer setNeedsDisplay];
 541        }
 542    }
 543    else
 544    {
 545        if([self zoom] > [self maxZoom])
 546            [self setZoom:[self maxZoom]];
 547        if([self zoom] < [self minZoom])
 548            [self setZoom:[self minZoom]];
 549    }
 550}
 551
 552/// \bug magic strings embedded in code
 553- (void)animatedZoomStep:(NSTimer *)timer
 554{
 555	double zoomIncr = [[[timer userInfo] objectForKey:@"zoomIncr"] doubleValue];
 556	double targetZoom = [[[timer userInfo] objectForKey:@"targetZoom"] doubleValue];
 557    
 558	NSDictionary *userInfo = [[[timer userInfo] retain] autorelease];
 559	id<RMMapContentsAnimationCallback> callback = [userInfo objectForKey:@"callback"];
 560
 561	if ((zoomIncr > 0 && [self zoom] >= targetZoom-1.0e-6) || (zoomIncr < 0 && [self zoom] <= targetZoom+1.0e-6))
 562	{
 563        if ( [self zoom] != targetZoom ) [self setZoom:targetZoom];
 564		[timer invalidate];	// ASAP
 565		if ([callback respondsToSelector:@selector(animationFinishedWithZoomFactor:near:)])
 566		{
 567			[callback animationFinishedWithZoomFactor:[[userInfo objectForKey:@"factor"] floatValue] near:[[userInfo objectForKey:@"pivot"] CGPointValue]];
 568		}
 569	}
 570	else
 571	{
 572		float zoomFactorStep = exp2f(zoomIncr);
 573		[self zoomByFactor:zoomFactorStep near:[[[timer userInfo] objectForKey:@"pivot"] CGPointValue] animated:NO];
 574		if ([callback respondsToSelector:@selector(animationStepped)])
 575		{
 576			[callback animationStepped];
 577		}
 578	}
 579}
 580
 581
 582- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot
 583{
 584	[self zoomInToNextNativeZoomAt:pivot animated:NO];
 585}
 586
 587- (float)nextNativeZoomFactor
 588{
 589	float newZoom = fmin(floorf([self zoom] + 1.0), [self maxZoom]);
 590	return exp2f(newZoom - [self zoom]);
 591}
 592
 593- (float)prevNativeZoomFactor
 594{
 595	float newZoom = fmax(floorf([self zoom] - 1.0), [self minZoom]);
 596	return exp2f(newZoom - [self zoom]);
 597}
 598
 599/// \deprecated appears to be unused
 600- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot animated:(BOOL) animated
 601{
 602	// Calculate rounded zoom
 603	float newZoom = fmin(floorf([self zoom] + 1.0), [self maxZoom]);
 604	RMLog(@"[self minZoom] %f [self zoom] %f [self maxZoom] %f newzoom %f", [self minZoom], [self zoom], [self maxZoom], newZoom);
 605	
 606	float factor = exp2f(newZoom - [self zoom]);
 607	[self zoomByFactor:factor near:pivot animated:animated];
 608}
 609
 610/// \deprecated appears to be unused except by zoomOutToNextNativeZoomAt:
 611- (void)zoomOutToNextNativeZoomAt:(CGPoint) pivot animated:(BOOL) animated {
 612	// Calculate rounded zoom
 613	float newZoom = fmax(ceilf([self zoom] - 1.0), [self minZoom]);
 614	RMLog(@"[self minZoom] %f [self zoom] %f [self maxZoom] %f newzoom %f", [self minZoom], [self zoom], [self maxZoom], newZoom);
 615	
 616	float factor = exp2f(newZoom - [self zoom]);
 617	[self zoomByFactor:factor near:pivot animated:animated];
 618}
 619
 620/// \deprecated appears to be unused
 621- (void)zoomOutToNextNativeZoomAt:(CGPoint) pivot {
 622	[self zoomOutToNextNativeZoomAt: pivot animated: FALSE];
 623}
 624 
 625
 626- (void) drawRect: (CGRect) aRect
 627{
 628	[renderer drawRect:aRect];
 629}
 630
 631-(void)removeAllCachedImages
 632{
 633	[tileSource removeAllCachedImages];
 634}
 635
 636
 637#pragma mark Properties
 638
 639- (void) setTileSource: (id<RMTileSource>)newTileSource
 640{
 641	if (tileSource == newTileSource)
 642		return;
 643	
 644	RMCachedTileSource *newCachedTileSource = [RMCachedTileSource cachedTileSourceWithSource:newTileSource];
 645
 646	newCachedTileSource = [newCachedTileSource retain];
 647	[tileSource release];
 648	tileSource = newCachedTileSource;
 649
 650        NSAssert(([tileSource minZoom] - minZoom) <= 1.0, @"Graphics & memory are overly taxed if [contents minZoom] is more than 1.5 smaller than [tileSource minZoom]");
 651	
 652	[projection release];
 653	projection = [[tileSource projection] retain];
 654	
 655	[mercatorToTileProjection release];
 656	mercatorToTileProjection = [[tileSource mercatorToTileProjection] retain];
 657
 658	[imagesOnScreen setTileSource:tileSource];
 659
 660        [tileLoader reset];
 661	[tileLoader reload];
 662}
 663
 664- (id<RMTileSource>) tileSource
 665{
 666	return [[tileSource retain] autorelease];
 667}
 668
 669- (void) setRenderer: (RMMapRenderer*) newRenderer
 670{
 671	if (renderer == newRenderer)
 672		return;
 673	
 674	[imagesOnScreen setDelegate:newRenderer];
 675	
 676	[[renderer layer] removeFromSuperlayer];
 677	[renderer release];
 678	
 679	renderer = [newRenderer retain];
 680	
 681	if (renderer == nil)
 682		return;
 683	
 684	//	CGRect rect = [self screenBounds];
 685	//	RMLog(@"%f %f %f %f", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
 686	[[renderer layer] setFrame:[self screenBounds]];
 687	
 688	if (background != nil)
 689		[layer insertSublayer:[renderer layer] above:background];
 690	else if (overlay != nil)
 691		[layer insertSublayer:[renderer layer] below:overlay];
 692	else
 693		[layer insertSublayer:[renderer layer] atIndex: 0];
 694}
 695
 696- (RMMapRenderer *)renderer
 697{
 698	return [[renderer retain] autorelease];
 699}
 700
 701- (void) setBackground: (RMMapLayer*) aLayer
 702{
 703	if (background == aLayer) return;
 704	
 705	if (background != nil)
 706	{
 707		[background release];
 708		[background removeFromSuperlayer];		
 709	}
 710	
 711	background = [aLayer retain];
 712	
 713	if (background == nil)
 714		return;
 715	
 716	background.frame = [self screenBounds];
 717	
 718	if ([renderer layer] != nil)
 719		[layer insertSublayer:background below:[renderer layer]];
 720	else if (overlay != nil)
 721		[layer insertSublayer:background below:overlay];
 722	else
 723		[layer insertSublayer:[renderer layer] atIndex: 0];
 724}
 725
 726- (RMMapLayer *)background
 727{
 728	return [[background retain] autorelease];
 729}
 730
 731- (void) setOverlay: (RMLayerCollection*) aLayer
 732{
 733	if (overlay == aLayer) return;
 734	
 735	if (overlay != nil)
 736	{
 737		[overlay release];
 738		[overlay removeFromSuperlayer];		
 739	}
 740	
 741	overlay = [aLayer retain];
 742	
 743	if (overlay == nil)
 744		return;
 745	
 746	overlay.frame = [self screenBounds];
 747	
 748	if ([renderer layer] != nil)
 749		[layer insertSublayer:overlay above:[renderer layer]];
 750	else if (background != nil)
 751		[layer insertSublayer:overlay above:background];
 752	else
 753		[layer insertSublayer:[renderer layer] atIndex: 0];
 754	
 755	/* Test to make sure the overlay is working.
 756	CALayer *testLayer = [[CALayer alloc] init];
 757	
 758	[testLayer setFrame:CGRectMake(100, 100, 200, 200)];
 759	[testLayer setBackgroundColor:[[UIColor brownColor] CGColor]];
 760	
 761	RMLog(@"added test layer");
 762	[overlay addSublayer:testLayer];*/
 763}
 764
 765- (RMLayerCollection *)overlay
 766{
 767	return [[overlay retain] autorelease];
 768}
 769
 770- (CLLocationCoordinate2D) mapCenter
 771{
 772	RMProjectedPoint aPoint = [mercatorToScreenProjection projectedCenter];
 773	return [projection pointToLatLong:aPoint];
 774}
 775
 776-(void) setMapCenter: (CLLocationCoordinate2D) center
 777{
 778	[self moveToLatLong:center];
 779}
 780
 781- (RMProjectedPoint)centerProjectedPoint
 782{
 783	return [mercatorToScreenProjection projectedCenter];
 784}
 785
 786- (void)setCenterProjectedPoint:(RMProjectedPoint)projectedPoint
 787{
 788	[mercatorToScreenProjection setProjectedCenter:projectedPoint];
 789	[overlay correctPositionOfAllSublayers];
 790	[tileLoader reload];
 791	[renderer setNeedsDisplay];
 792	[overlay setNeedsDisplay];
 793}
 794
 795-(RMProjectedRect) projectedBounds
 796{
 797	return [mercatorToScreenProjection projectedBounds];
 798}
 799-(void) setProjectedBounds: (RMProjectedRect) boundsRect
 800{
 801	[mercatorToScreenProjection setProjectedBounds:boundsRect];
 802}
 803
 804-(RMTileRect) tileBounds
 805{
 806    return [mercatorToTileProjection projectRect:[mercatorToScreenProjection projectedBounds] 
 807                                         atScale:[self scaledMetersPerPixel]];
 808}
 809
 810-(CGRect) screenBounds
 811{
 812	if (mercatorToScreenProjection != nil)
 813		return [mercatorToScreenProjection screenBounds];
 814	else
 815		return CGRectZero;
 816}
 817
 818-(float) metersPerPixel
 819{
 820	return [mercatorToScreenProjection metersPerPixel];
 821}
 822
 823-(void) setMetersPerPixel: (float) newMPP
 824{
 825        float zoomFactor = self.metersPerPixel / newMPP;
 826        CGPoint pivot = CGPointZero;
 827
 828        [mercatorToScreenProjection setMetersPerPixel:newMPP];
 829        [imagesOnScreen zoomByFactor:zoomFactor near:pivot];
 830        [tileLoader zoomByFactor:zoomFactor near:pivot];
 831        [overlay zoomByFactor:zoomFactor near:pivot];
 832        [overlay correctPositionOfAllSublayers];
 833        [renderer setNeedsDisplay];
 834}
 835
 836-(float) scaledMetersPerPixel
 837{
 838    return [mercatorToScreenProjection metersPerPixel] / screenScale;
 839}
 840
 841- (void)setScaledMetersPerPixel:(float)newMPP {
 842    [self setMetersPerPixel:newMPP * screenScale];
 843}
 844
 845-(void)setMaxZoom:(float)newMaxZoom
 846{
 847	maxZoom = newMaxZoom;
 848}
 849
 850-(void)setMinZoom:(float)newMinZoom
 851{
 852	minZoom = newMinZoom;
 853
 854        NSAssert(!tileSource || (([tileSource minZoom] - minZoom) <= 1.0), @"Graphics & memory are overly taxed if [contents minZoom] is more than 1.5 smaller than [tileSource minZoom]");
 855}
 856
 857-(float) zoom
 858{
 859        return [mercatorToTileProjection calculateZoomFromScale:[self scaledMetersPerPixel]];
 860}
 861
 862/// if #zoom is outside of range #minZoom to #maxZoom, zoom level is clamped to that range.
 863-(void) setZoom: (float) zoom
 864{
 865        zoom = (zoom > maxZoom) ? maxZoom : zoom;
 866        zoom = (zoom < minZoom) ? minZoom : zoom;
 867
 868        float scale = [mercatorToTileProjection calculateScaleFromZoom:zoom];
 869
 870        [self setScaledMetersPerPixel:scale];
 871}
 872
 873-(RMTileImageSet*) imagesOnScreen
 874{
 875	return [[imagesOnScreen retain] autorelease];
 876}
 877
 878-(RMTileLoader*) tileLoader
 879{
 880	return [[tileLoader retain] autorelease];
 881}
 882
 883-(RMProjection*) projection
 884{
 885	return [[projection retain] autorelease];
 886}
 887-(id<RMMercatorToTileProjection>) mercatorToTileProjection
 888{
 889	return [[mercatorToTileProjection retain] autorelease];
 890}
 891-(RMMercatorToScreenProjection*) mercatorToScreenProjection
 892{
 893	return [[mercatorToScreenProjection retain] autorelease];
 894}
 895
 896- (CALayer *)layer
 897{
 898	return [[layer retain] autorelease];
 899}
 900
 901static BOOL _performExpensiveOperations = YES;
 902+ (BOOL) performExpensiveOperations
 903{
 904	return _performExpensiveOperations;
 905}
 906+ (void) setPerformExpensiveOperations: (BOOL)p
 907{
 908	if (p == _performExpensiveOperations)
 909		return;
 910	
 911	_performExpensiveOperations = p;
 912
 913	if (p)
 914		[[NSNotificationCenter defaultCenter] postNotificationName:RMResumeExpensiveOperations object:self];
 915	else
 916		[[NSNotificationCenter defaultCenter] postNotificationName:RMSuspendExpensiveOperations object:self];
 917}
 918
 919#pragma mark LatLng/Pixel translation functions
 920
 921- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong
 922{	
 923	return [mercatorToScreenProjection projectXYPoint:[projection latLongToPoint:latlong]];
 924}
 925
 926- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong withMetersPerPixel:(float)aScale
 927{	
 928	return [mercatorToScreenProjection projectXYPoint:[projection latLongToPoint:latlong] withMetersPerPixel:aScale];
 929}
 930
 931- (RMTilePoint)latLongToTilePoint:(CLLocationCoordinate2D)latlong withMetersPerPixel:(float)aScale
 932{
 933        return [mercatorToTileProjection project:[projection latLongToPoint:latlong] atZoom:aScale];
 934}
 935
 936- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel
 937{
 938	return [projection pointToLatLong:[mercatorToScreenProjection projectScreenPointToXY:aPixel]];
 939}
 940
 941- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel withMetersPerPixel:(float)aScale
 942{
 943	return [projection pointToLatLong:[mercatorToScreenProjection projectScreenPointToXY:aPixel withMetersPerPixel:aScale]];
 944}
 945
 946- (double)scaleDenominator {
 947	double routemeMetersPerPixel = [self metersPerPixel];
 948	double iphoneMillimetersPerPixel = kiPhoneMilimeteresPerPixel;
 949	double truescaleDenominator =  routemeMetersPerPixel / (0.001 * iphoneMillimetersPerPixel) ;
 950	return truescaleDenominator;
 951}
 952
 953#pragma mark Zoom With Bounds
 954- (void)zoomWithLatLngBoundsNorthEast:(CLLocationCoordinate2D)ne SouthWest:(CLLocationCoordinate2D)sw
 955{
 956	if(ne.latitude == sw.latitude && ne.longitude == sw.longitude)//There are no bounds, probably only one marker.
 957	{
 958		RMProjectedRect zoomRect;
 959		RMProjectedPoint myOrigin = [projection latLongToPoint:sw];
 960		//Default is with scale = 2.0 mercators/pixel
 961		zoomRect.size.width = [self screenBounds].size.width * 2.0;
 962		zoomRect.size.height = [self screenBounds].size.height * 2.0;
 963		myOrigin.easting = myOrigin.easting - (zoomRect.size.width / 2);
 964		myOrigin.northing = myOrigin.northing - (zoomRect.size.height / 2);
 965		zoomRect.origin = myOrigin;
 966		[self zoomWithRMMercatorRectBounds:zoomRect];
 967	}
 968	else
 969	{
 970		//convert ne/sw into RMMercatorRect and call zoomWithBounds
 971		float pixelBuffer = kZoomRectPixelBuffer;
 972		CLLocationCoordinate2D midpoint = {
 973			.latitude = (ne.latitude + sw.latitude) / 2,
 974			.longitude = (ne.longitude + sw.longitude) / 2
 975		};
 976		RMProjectedPoint myOrigin = [projection latLongToPoint:midpoint];
 977		RMProjectedPoint nePoint = [projection latLongToPoint:ne];
 978		RMProjectedPoint swPoint = [projection latLongToPoint:sw];
 979		RMProjectedPoint myPoint = {.easting = nePoint.easting - swPoint.easting, .northing = nePoint.northing - swPoint.northing};
 980		//Create the new zoom layout
 981		RMProjectedRect zoomRect;
 982		//Default is with scale = 2.0 mercators/pixel
 983		zoomRect.size.width = [self screenBounds].size.width * 2.0;
 984		zoomRect.size.height = [self screenBounds].size.height * 2.0;
 985		if((myPoint.easting / ([self screenBounds].size.width)) < (myPoint.northing / ([self screenBounds].size.height)))
 986		{
 987			if((myPoint.northing / ([self screenBounds].size.height - pixelBuffer)) > 1)
 988			{
 989				zoomRect.size.width = [self screenBounds].size.width * (myPoint.northing / ([self screenBounds].size.height - pixelBuffer));
 990				zoomRect.size.height = [self screenBounds].size.height * (myPoint.northing / ([self screenBounds].size.height - pixelBuffer));
 991			}
 992		}
 993		else
 994		{
 995			if((myPoint.easting / ([self screenBounds].size.width - pixelBuffer)) > 1)
 996			{
 997				zoomRect.size.width = [self screenBounds].size.width * (myPoint.easting / ([self screenBounds].size.width - pixelBuffer));
 998				zoomRect.size.height = [self screenBounds].size.height * (myPoint.easting / ([self screenBounds].size.width - pixelBuffer));
 999			}
1000		}
1001		myOrigin.easting = myOrigin.easting - (zoomRect.size.width / 2);
1002		myOrigin.northing = myOrigin.northing - (zoomRect.size.height / 2);
1003		RMLog(@"Origin is calculated at: %f, %f", [projection pointToLatLong:myOrigin].latitude, [projection pointToLatLong:myOrigin].longitude);
1004		/*It gets all messed up if our origin is lower than the lowest place on the map, so we check.
1005		 if(myOrigin.northing < -19971868.880409)
1006		 {
1007		 myOrigin.northing = -19971868.880409;
1008		 }*/
1009		zoomRect.origin = myOrigin;
1010		[self zoomWithRMMercatorRectBounds:zoomRect];
1011	}
1012}
1013
1014- (void)zoomWithRMMercatorRectBounds:(RMProjectedRect)bounds
1015{
1016	[self setProjectedBounds:bounds];
1017	[overlay correctPositionOfAllSublayers];
1018	[tileLoader clearLoadedBounds];
1019	[tileLoader updateLoadedImages];
1020	[renderer setNeedsDisplay];
1021}
1022
1023
1024#pragma mark Markers and overlays
1025
1026// Move overlays stuff here - at the moment overlay stuff is above...
1027
1028- (RMSphericalTrapezium) latitudeLongitudeBoundingBoxForScreen
1029{
1030	CGRect rect = [mercatorToScreenProjection screenBounds];
1031	
1032	return [self latitudeLongitudeBoundingBoxFor:rect];
1033}
1034
1035- (RMSphericalTrapezium) latitudeLongitudeBoundingBoxFor:(CGRect) rect
1036{	
1037	RMSphericalTrapezium boundingBox;
1038	CGPoint northwestScreen = rect.origin;
1039	
1040	CGPoint southeastScreen;
1041	southeastScreen.x = rect.origin.x + rect.size.width;
1042	southeastScreen.y = rect.origin.y + rect.size.height;
1043	
1044	CGPoint northeastScreen, southwestScreen;
1045	northeastScreen.x = southeastScreen.x;
1046	northeastScreen.y = northwestScreen.y;
1047	southwestScreen.x = northwestScreen.x;
1048	southwestScreen.y = southeastScreen.y;
1049	
1050	CLLocationCoordinate2D northeastLL, northwestLL, southeastLL, southwestLL;
1051	northeastLL = [self pixelToLatLong:northeastScreen];
1052	northwestLL = [self pixelToLatLong:northwestScreen];
1053	southeastLL = [self pixelToLatLong:southeastScreen];
1054	southwestLL = [self pixelToLatLong:southwestScreen];
1055	
1056	boundingBox.northeast.latitude = fmax(northeastLL.latitude, northwestLL.latitude);
1057	boundingBox.southwest.latitude = fmin(southeastLL.latitude, southwestLL.latitude);
1058	
1059	// westerly computations:
1060	// -179, -178 -> -179 (min)
1061	// -179, 179  -> 179 (max)
1062	if (fabs(northwestLL.longitude - southwestLL.longitude) <= kMaxLong)
1063		boundingBox.southwest.longitude = fmin(northwestLL.longitude, southwestLL.longitude);
1064	else
1065		boundingBox.southwest.longitude = fmax(northwestLL.longitude, southwestLL.longitude);
1066	
1067	if (fabs(northeastLL.longitude - southeastLL.longitude) <= kMaxLong)
1068		boundingBox.northeast.longitude = fmax(northeastLL.longitude, southeastLL.longitude);
1069	else
1070		boundingBox.northeast.longitude = fmin(northeastLL.longitude, southeastLL.longitude);
1071
1072	return boundingBox;
1073}
1074
1075- (void) tilesUpdatedRegion:(CGRect)region
1076{
1077	if(delegateHasRegionUpdate)
1078	{
1079		RMSphericalTrapezium locationBounds  = [self latitudeLongitudeBoundingBoxFor:region];
1080		[tilesUpdateDelegate regionUpdate:locationBounds];
1081	}
1082}
1083- (void) printDebuggingInformation
1084{
1085	[imagesOnScreen printDebuggingInformation];
1086}
1087
1088@dynamic tilesUpdateDelegate;
1089
1090- (void) setTilesUpdateDelegate: (id<RMTilesUpdateDelegate>) _tilesUpdateDelegate
1091{
1092	if (tilesUpdateDelegate == _tilesUpdateDelegate) return;
1093	tilesUpdateDelegate= _tilesUpdateDelegate;
1094	//RMLog(@"Delegate type:%@",[(NSObject *) tilesUpdateDelegate description]);
1095	delegateHasRegionUpdate  = [(NSObject*) tilesUpdateDelegate respondsToSelector: @selector(regionUpdate:)];
1096}
1097
1098- (id<RMTilesUpdateDelegate>) tilesUpdateDelegate
1099{
1100	return tilesUpdateDelegate;
1101}
1102
1103- (void)setRotation:(float)angle
1104{ 
1105	[overlay setRotationOfAllSublayers:(-angle)]; // rotate back markers and paths if theirs allowRotate=NO
1106}
1107
1108- (short)tileDepth {
1109	return imagesOnScreen.tileDepth;
1110}
1111
1112- (void)setTileDepth:(short)value {
1113	imagesOnScreen.tileDepth = value;
1114}
1115
1116- (BOOL)fullyLoaded {
1117	return imagesOnScreen.fullyLoaded;
1118}
1119
1120@end