/MapView/Map/RMMapContents.m
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