Authored by Justin R. Miller

added user location services

@@ -73,6 +73,9 @@ @@ -73,6 +73,9 @@
73 // This is for the QuadTree. Don't mess this up. 73 // This is for the QuadTree. Don't mess this up.
74 @property (nonatomic, assign) RMQuadTreeNode *quadTreeNode; 74 @property (nonatomic, assign) RMQuadTreeNode *quadTreeNode;
75 75
  76 +// This is for filtering framework-provided annotations.
  77 +@property (nonatomic, assign, readonly) BOOL isUserLocationAnnotation;
  78 +
76 #pragma mark - 79 #pragma mark -
77 80
78 + (id)annotationWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle; 81 + (id)annotationWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle;
@@ -50,6 +50,7 @@ @@ -50,6 +50,7 @@
50 @synthesize enabled, clusteringEnabled; 50 @synthesize enabled, clusteringEnabled;
51 @synthesize position; 51 @synthesize position;
52 @synthesize quadTreeNode; 52 @synthesize quadTreeNode;
  53 +@synthesize isUserLocationAnnotation;
53 54
54 + (id)annotationWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle 55 + (id)annotationWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle
55 { 56 {
@@ -178,6 +179,11 @@ @@ -178,6 +179,11 @@
178 return (layer != nil); 179 return (layer != nil);
179 } 180 }
180 181
  182 +- (void)setIsUserLocationAnnotation:(BOOL)flag
  183 +{
  184 + isUserLocationAnnotation = flag;
  185 +}
  186 +
181 #pragma mark - 187 #pragma mark -
182 188
183 - (void)setBoundingBoxCoordinatesSouthWest:(CLLocationCoordinate2D)southWest northEast:(CLLocationCoordinate2D)northEast 189 - (void)setBoundingBoxCoordinatesSouthWest:(CLLocationCoordinate2D)southWest northEast:(CLLocationCoordinate2D)northEast
@@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
23 if (!(self = [super initWithFrame:frame])) 23 if (!(self = [super initWithFrame:frame]))
24 return nil; 24 return nil;
25 25
26 - self.layer.masksToBounds = YES; 26 + self.layer.masksToBounds = NO;
27 27
28 return self; 28 return self;
29 } 29 }
@@ -38,6 +38,10 @@ @@ -38,6 +38,10 @@
38 #import "RMMapScrollView.h" 38 #import "RMMapScrollView.h"
39 #import "RMTileSourcesContainer.h" 39 #import "RMTileSourcesContainer.h"
40 40
  41 +#define kRMUserLocationAnnotationTypeName @"RMUserLocationAnnotation"
  42 +#define kRMTrackingHaloAnnotationTypeName @"RMTrackingHaloAnnotation"
  43 +#define kRMAccuracyCircleAnnotationTypeName @"RMAccuracyCircleAnnotation"
  44 +
41 @class RMProjection; 45 @class RMProjection;
42 @class RMFractalTileProjection; 46 @class RMFractalTileProjection;
43 @class RMTileCache; 47 @class RMTileCache;
@@ -47,6 +51,7 @@ @@ -47,6 +51,7 @@
47 @class RMMarker; 51 @class RMMarker;
48 @class RMAnnotation; 52 @class RMAnnotation;
49 @class RMQuadTree; 53 @class RMQuadTree;
  54 +@class RMUserLocation;
50 55
51 56
52 // constants for boundingMask 57 // constants for boundingMask
@@ -64,7 +69,7 @@ typedef enum : NSUInteger { @@ -64,7 +69,7 @@ typedef enum : NSUInteger {
64 } RMMapDecelerationMode; 69 } RMMapDecelerationMode;
65 70
66 71
67 -@interface RMMapView : UIView <UIScrollViewDelegate, UIGestureRecognizerDelegate, RMMapScrollViewDelegate> 72 +@interface RMMapView : UIView <UIScrollViewDelegate, UIGestureRecognizerDelegate, RMMapScrollViewDelegate, CLLocationManagerDelegate>
68 73
69 @property (nonatomic, assign) id <RMMapViewDelegate> delegate; 74 @property (nonatomic, assign) id <RMMapViewDelegate> delegate;
70 75
@@ -83,6 +88,11 @@ typedef enum : NSUInteger { @@ -83,6 +88,11 @@ typedef enum : NSUInteger {
83 @property (nonatomic, assign) BOOL adjustTilesForRetinaDisplay; 88 @property (nonatomic, assign) BOOL adjustTilesForRetinaDisplay;
84 @property (nonatomic, readonly) float adjustedZoomForRetinaDisplay; // takes adjustTilesForRetinaDisplay and screen scale into account 89 @property (nonatomic, readonly) float adjustedZoomForRetinaDisplay; // takes adjustTilesForRetinaDisplay and screen scale into account
85 90
  91 +@property (nonatomic) BOOL showsUserLocation;
  92 +@property (nonatomic, readonly, retain) RMUserLocation *userLocation;
  93 +@property (nonatomic, readonly, getter=isUserLocationVisible) BOOL userLocationVisible;
  94 +@property (nonatomic) RMUserTrackingMode userTrackingMode;
  95 +
86 // take missing tiles from lower zoom levels, up to #missingTilesDepth zoom levels (defaults to 0, which disables this feature) 96 // take missing tiles from lower zoom levels, up to #missingTilesDepth zoom levels (defaults to 0, which disables this feature)
87 @property (nonatomic, assign) NSUInteger missingTilesDepth; 97 @property (nonatomic, assign) NSUInteger missingTilesDepth;
88 98
@@ -235,4 +245,9 @@ typedef enum : NSUInteger { @@ -235,4 +245,9 @@ typedef enum : NSUInteger {
235 245
236 - (RMSphericalTrapezium)latitudeLongitudeBoundingBoxForTile:(RMTile)aTile; 246 - (RMSphericalTrapezium)latitudeLongitudeBoundingBoxForTile:(RMTile)aTile;
237 247
  248 +#pragma mark -
  249 +#pragma mark User Location
  250 +
  251 +- (void)setUserTrackingMode:(RMUserTrackingMode)mode animated:(BOOL)animated;
  252 +
238 @end 253 @end
@@ -33,6 +33,7 @@ @@ -33,6 +33,7 @@
33 #import "RMProjection.h" 33 #import "RMProjection.h"
34 #import "RMMarker.h" 34 #import "RMMarker.h"
35 #import "RMPath.h" 35 #import "RMPath.h"
  36 +#import "RMCircle.h"
36 #import "RMShape.h" 37 #import "RMShape.h"
37 #import "RMAnnotation.h" 38 #import "RMAnnotation.h"
38 #import "RMQuadTree.h" 39 #import "RMQuadTree.h"
@@ -46,6 +47,8 @@ @@ -46,6 +47,8 @@
46 #import "RMMapTiledLayerView.h" 47 #import "RMMapTiledLayerView.h"
47 #import "RMMapOverlayView.h" 48 #import "RMMapOverlayView.h"
48 49
  50 +#import "RMUserLocation.h"
  51 +
49 #pragma mark --- begin constants ---- 52 #pragma mark --- begin constants ----
50 53
51 #define kZoomRectPixelBuffer 150.0 54 #define kZoomRectPixelBuffer 150.0
@@ -61,6 +64,8 @@ @@ -61,6 +64,8 @@
61 64
62 @interface RMMapView (PrivateMethods) 65 @interface RMMapView (PrivateMethods)
63 66
  67 +@property (nonatomic, retain) RMUserLocation *userLocation;
  68 +
64 - (void)createMapView; 69 - (void)createMapView;
65 70
66 - (void)correctPositionOfAllAnnotations; 71 - (void)correctPositionOfAllAnnotations;
@@ -72,6 +77,24 @@ @@ -72,6 +77,24 @@
72 77
73 #pragma mark - 78 #pragma mark -
74 79
  80 +@interface RMUserLocation (PrivateMethods)
  81 +
  82 +@property (nonatomic, getter=isUpdating) BOOL updating;
  83 +@property (nonatomic, retain) CLLocation *location;
  84 +@property (nonatomic, retain) CLHeading *heading;
  85 +
  86 +@end
  87 +
  88 +#pragma mark -
  89 +
  90 +@interface RMAnnotation (PrivateMethods)
  91 +
  92 +@property (nonatomic, assign) BOOL isUserLocationAnnotation;
  93 +
  94 +@end
  95 +
  96 +#pragma mark -
  97 +
75 @implementation RMMapView 98 @implementation RMMapView
76 { 99 {
77 id <RMMapViewDelegate> _delegate; 100 id <RMMapViewDelegate> _delegate;
@@ -96,6 +119,11 @@ @@ -96,6 +119,11 @@
96 BOOL _delegateHasLayerForAnnotation; 119 BOOL _delegateHasLayerForAnnotation;
97 BOOL _delegateHasWillHideLayerForAnnotation; 120 BOOL _delegateHasWillHideLayerForAnnotation;
98 BOOL _delegateHasDidHideLayerForAnnotation; 121 BOOL _delegateHasDidHideLayerForAnnotation;
  122 + BOOL _delegateHasWillStartLocatingUser;
  123 + BOOL _delegateHasDidStopLocatingUser;
  124 + BOOL _delegateHasDidUpdateUserLocation;
  125 + BOOL _delegateHasDidFailToLocateUserWithError;
  126 + BOOL _delegateHasDidChangeUserTrackingMode;
99 127
100 UIView *_backgroundView; 128 UIView *_backgroundView;
101 RMMapScrollView *_mapScrollView; 129 RMMapScrollView *_mapScrollView;
@@ -122,6 +150,14 @@ @@ -122,6 +150,14 @@
122 150
123 CGPoint _lastDraggingTranslation; 151 CGPoint _lastDraggingTranslation;
124 RMAnnotation *_draggedAnnotation; 152 RMAnnotation *_draggedAnnotation;
  153 +
  154 + CLLocationManager *locationManager;
  155 + RMUserLocation *userLocation;
  156 + BOOL showsUserLocation;
  157 + RMUserTrackingMode userTrackingMode;
  158 +
  159 + UIImageView *userLocationTrackingView;
  160 + UIImageView *userHeadingTrackingView;
125 } 161 }
126 162
127 @synthesize decelerationMode = _decelerationMode; 163 @synthesize decelerationMode = _decelerationMode;
@@ -136,6 +172,7 @@ @@ -136,6 +172,7 @@
136 @synthesize positionClusterMarkersAtTheGravityCenter = _positionClusterMarkersAtTheGravityCenter; 172 @synthesize positionClusterMarkersAtTheGravityCenter = _positionClusterMarkersAtTheGravityCenter;
137 @synthesize clusterMarkerSize = _clusterMarkerSize, clusterAreaSize = _clusterAreaSize; 173 @synthesize clusterMarkerSize = _clusterMarkerSize, clusterAreaSize = _clusterAreaSize;
138 @synthesize adjustTilesForRetinaDisplay = _adjustTilesForRetinaDisplay; 174 @synthesize adjustTilesForRetinaDisplay = _adjustTilesForRetinaDisplay;
  175 +@synthesize userLocation, showsUserLocation, userTrackingMode;
139 @synthesize missingTilesDepth = _missingTilesDepth; 176 @synthesize missingTilesDepth = _missingTilesDepth;
140 @synthesize debugTiles = _debugTiles; 177 @synthesize debugTiles = _debugTiles;
141 178
@@ -157,6 +194,8 @@ @@ -157,6 +194,8 @@
157 194
158 self.backgroundColor = [UIColor grayColor]; 195 self.backgroundColor = [UIColor grayColor];
159 196
  197 + self.clipsToBounds = YES;
  198 +
160 _tileSourcesContainer = [RMTileSourcesContainer new]; 199 _tileSourcesContainer = [RMTileSourcesContainer new];
161 _tiledLayersSuperview = nil; 200 _tiledLayersSuperview = nil;
162 201
@@ -311,6 +350,10 @@ @@ -311,6 +350,10 @@
311 [_projection release]; _projection = nil; 350 [_projection release]; _projection = nil;
312 [_mercatorToTileProjection release]; _mercatorToTileProjection = nil; 351 [_mercatorToTileProjection release]; _mercatorToTileProjection = nil;
313 [self setTileCache:nil]; 352 [self setTileCache:nil];
  353 + [locationManager release]; locationManager = nil;
  354 + [userLocation release]; userLocation = nil;
  355 + [userLocationTrackingView release]; userLocationTrackingView = nil;
  356 + [userHeadingTrackingView release]; userHeadingTrackingView = nil;
314 [super dealloc]; 357 [super dealloc];
315 } 358 }
316 359
@@ -375,6 +418,12 @@ @@ -375,6 +418,12 @@
375 _delegateHasLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:layerForAnnotation:)]; 418 _delegateHasLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:layerForAnnotation:)];
376 _delegateHasWillHideLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:willHideLayerForAnnotation:)]; 419 _delegateHasWillHideLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:willHideLayerForAnnotation:)];
377 _delegateHasDidHideLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:didHideLayerForAnnotation:)]; 420 _delegateHasDidHideLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:didHideLayerForAnnotation:)];
  421 +
  422 + _delegateHasWillStartLocatingUser = [_delegate respondsToSelector:@selector(mapViewWillStartLocatingUser:)];
  423 + _delegateHasDidStopLocatingUser = [_delegate respondsToSelector:@selector(mapViewDidStopLocatingUser:)];
  424 + _delegateHasDidUpdateUserLocation = [_delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)];
  425 + _delegateHasDidFailToLocateUserWithError = [_delegate respondsToSelector:@selector(mapView:didFailToLocateUserWithError:)];
  426 + _delegateHasDidChangeUserTrackingMode = [_delegate respondsToSelector:@selector(mapView:didChangeUserTrackingMode:animated:)];
378 } 427 }
379 428
380 #pragma mark - 429 #pragma mark -
@@ -768,6 +817,9 @@ @@ -768,6 +817,9 @@
768 817
769 - (void)zoomInToNextNativeZoomAt:(CGPoint)pivot animated:(BOOL)animated 818 - (void)zoomInToNextNativeZoomAt:(CGPoint)pivot animated:(BOOL)animated
770 { 819 {
  820 + if (self.userTrackingMode != RMUserTrackingModeNone && ! CGPointEqualToPoint(pivot, self.center))
  821 + self.userTrackingMode = RMUserTrackingModeNone;
  822 +
771 // Calculate rounded zoom 823 // Calculate rounded zoom
772 float newZoom = fmin(ceilf([self zoom]) + 0.99, [self maxZoom]); 824 float newZoom = fmin(ceilf([self zoom]) + 0.99, [self maxZoom]);
773 825
@@ -930,6 +982,7 @@ @@ -930,6 +982,7 @@
930 _mapScrollView.minimumZoomScale = exp2f([self minZoom]); 982 _mapScrollView.minimumZoomScale = exp2f([self minZoom]);
931 _mapScrollView.maximumZoomScale = exp2f([self maxZoom]); 983 _mapScrollView.maximumZoomScale = exp2f([self maxZoom]);
932 _mapScrollView.contentOffset = CGPointMake(0.0, 0.0); 984 _mapScrollView.contentOffset = CGPointMake(0.0, 0.0);
  985 + _mapScrollView.clipsToBounds = NO;
933 986
934 _tiledLayersSuperview = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, contentSize.width, contentSize.height)]; 987 _tiledLayersSuperview = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, contentSize.width, contentSize.height)];
935 _tiledLayersSuperview.userInteractionEnabled = NO; 988 _tiledLayersSuperview.userInteractionEnabled = NO;
@@ -1022,6 +1075,9 @@ @@ -1022,6 +1075,9 @@
1022 1075
1023 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView 1076 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
1024 { 1077 {
  1078 + if (self.userTrackingMode != RMUserTrackingModeNone)
  1079 + self.userTrackingMode = RMUserTrackingModeNone;
  1080 +
1025 if (_delegateHasBeforeMapMove) 1081 if (_delegateHasBeforeMapMove)
1026 [_delegate beforeMapMove:self]; 1082 [_delegate beforeMapMove:self];
1027 } 1083 }
@@ -1067,8 +1123,14 @@ @@ -1067,8 +1123,14 @@
1067 1123
1068 - (void)scrollViewDidZoom:(UIScrollView *)scrollView 1124 - (void)scrollViewDidZoom:(UIScrollView *)scrollView
1069 { 1125 {
  1126 + if (self.userTrackingMode != RMUserTrackingModeNone && scrollView.pinchGestureRecognizer.state == UIGestureRecognizerStateChanged)
  1127 + self.userTrackingMode = RMUserTrackingModeNone;
  1128 +
1070 [self correctPositionOfAllAnnotations]; 1129 [self correctPositionOfAllAnnotations];
1071 1130
  1131 + if (_zoom < 3 && self.userTrackingMode == RMUserTrackingModeFollowWithHeading)
  1132 + self.userTrackingMode = RMUserTrackingModeFollow;
  1133 +
1072 if (_delegateHasAfterMapZoom) 1134 if (_delegateHasAfterMapZoom)
1073 [_delegate afterMapZoom:self]; 1135 [_delegate afterMapZoom:self];
1074 } 1136 }
@@ -2200,15 +2262,18 @@ @@ -2200,15 +2262,18 @@
2200 2262
2201 for (RMAnnotation *annotation in previousVisibleAnnotations) 2263 for (RMAnnotation *annotation in previousVisibleAnnotations)
2202 { 2264 {
2203 - if (_delegateHasWillHideLayerForAnnotation)  
2204 - [_delegate mapView:self willHideLayerForAnnotation:annotation]; 2265 + if ( ! annotation.isUserLocationAnnotation)
  2266 + {
  2267 + if (_delegateHasWillHideLayerForAnnotation)
  2268 + [_delegate mapView:self willHideLayerForAnnotation:annotation];
2205 2269
2206 - annotation.layer = nil; 2270 + annotation.layer = nil;
2207 2271
2208 - if (_delegateHasDidHideLayerForAnnotation)  
2209 - [_delegate mapView:self didHideLayerForAnnotation:annotation]; 2272 + if (_delegateHasDidHideLayerForAnnotation)
  2273 + [_delegate mapView:self didHideLayerForAnnotation:annotation];
2210 2274
2211 - [_visibleAnnotations removeObject:annotation]; 2275 + [_visibleAnnotations removeObject:annotation];
  2276 + }
2212 } 2277 }
2213 2278
2214 [previousVisibleAnnotations release]; 2279 [previousVisibleAnnotations release];
@@ -2248,14 +2313,17 @@ @@ -2248,14 +2313,17 @@
2248 } 2313 }
2249 else 2314 else
2250 { 2315 {
2251 - if (_delegateHasWillHideLayerForAnnotation)  
2252 - [_delegate mapView:self willHideLayerForAnnotation:annotation]; 2316 + if ( ! annotation.isUserLocationAnnotation)
  2317 + {
  2318 + if (_delegateHasWillHideLayerForAnnotation)
  2319 + [_delegate mapView:self willHideLayerForAnnotation:annotation];
2253 2320
2254 - annotation.layer = nil;  
2255 - [_visibleAnnotations removeObject:annotation]; 2321 + annotation.layer = nil;
  2322 + [_visibleAnnotations removeObject:annotation];
2256 2323
2257 - if (_delegateHasDidHideLayerForAnnotation)  
2258 - [_delegate mapView:self didHideLayerForAnnotation:annotation]; 2324 + if (_delegateHasDidHideLayerForAnnotation)
  2325 + [_delegate mapView:self didHideLayerForAnnotation:annotation];
  2326 + }
2259 } 2327 }
2260 } 2328 }
2261 // RMLog(@"%d annotations on screen, %d total", [overlayView sublayersCount], [annotations count]); 2329 // RMLog(@"%d annotations on screen, %d total", [overlayView sublayersCount], [annotations count]);
@@ -2346,10 +2414,13 @@ @@ -2346,10 +2414,13 @@
2346 { 2414 {
2347 for (RMAnnotation *annotation in annotationsToRemove) 2415 for (RMAnnotation *annotation in annotationsToRemove)
2348 { 2416 {
2349 - [_annotations removeObject:annotation];  
2350 - [_visibleAnnotations removeObject:annotation];  
2351 - [self.quadTree removeAnnotation:annotation];  
2352 - annotation.layer = nil; 2417 + if ( ! annotation.isUserLocationAnnotation)
  2418 + {
  2419 + [_annotations removeObject:annotation];
  2420 + [_visibleAnnotations removeObject:annotation];
  2421 + [self.quadTree removeAnnotation:annotation];
  2422 + annotation.layer = nil;
  2423 + }
2353 } 2424 }
2354 } 2425 }
2355 2426
@@ -2358,25 +2429,401 @@ @@ -2358,25 +2429,401 @@
2358 2429
2359 - (void)removeAllAnnotations 2430 - (void)removeAllAnnotations
2360 { 2431 {
2361 - @synchronized (_annotations) 2432 + [self removeAnnotations:[_annotations allObjects]];
  2433 +}
  2434 +
  2435 +- (CGPoint)mapPositionForAnnotation:(RMAnnotation *)annotation
  2436 +{
  2437 + [self correctScreenPosition:annotation animated:NO];
  2438 + return annotation.position;
  2439 +}
  2440 +
  2441 +#pragma mark -
  2442 +#pragma mark User Location
  2443 +
  2444 +- (void)setShowsUserLocation:(BOOL)newShowsUserLocation
  2445 +{
  2446 + if (newShowsUserLocation == showsUserLocation)
  2447 + return;
  2448 +
  2449 + showsUserLocation = newShowsUserLocation;
  2450 +
  2451 + if (newShowsUserLocation)
  2452 + {
  2453 + if (_delegateHasWillStartLocatingUser)
  2454 + [_delegate mapViewWillStartLocatingUser:self];
  2455 +
  2456 + self.userLocation = [RMUserLocation annotationWithMapView:self coordinate:CLLocationCoordinate2DMake(0, 0) andTitle:nil];
  2457 +
  2458 + locationManager = [[CLLocationManager alloc] init];
  2459 + locationManager.headingFilter = 5;
  2460 + locationManager.delegate = self;
  2461 + [locationManager startUpdatingLocation];
  2462 + }
  2463 + else
2362 { 2464 {
  2465 + [locationManager stopUpdatingLocation];
  2466 + [locationManager stopUpdatingHeading];
  2467 + locationManager.delegate = nil;
  2468 + [locationManager release];
  2469 + locationManager = nil;
  2470 +
  2471 + if (_delegateHasDidStopLocatingUser)
  2472 + [_delegate mapViewDidStopLocatingUser:self];
  2473 +
  2474 + [self setUserTrackingMode:RMUserTrackingModeNone animated:YES];
  2475 +
  2476 + NSMutableArray *annotationsToRemove = [NSMutableArray array];
  2477 +
2363 for (RMAnnotation *annotation in _annotations) 2478 for (RMAnnotation *annotation in _annotations)
  2479 + if (annotation.isUserLocationAnnotation)
  2480 + [annotationsToRemove addObject:annotation];
  2481 +
  2482 + for (RMAnnotation *annotationToRemove in annotationsToRemove)
  2483 + [self removeAnnotation:annotationToRemove];
  2484 +
  2485 + self.userLocation = nil;
  2486 + }
  2487 +}
  2488 +
  2489 +- (void)setUserLocation:(RMUserLocation *)newUserLocation
  2490 +{
  2491 + if ( ! [newUserLocation isEqual:userLocation])
  2492 + {
  2493 + [userLocation release];
  2494 + userLocation = [newUserLocation retain];
  2495 + }
  2496 +}
  2497 +
  2498 +- (BOOL)isUserLocationVisible
  2499 +{
  2500 + if (userLocation)
  2501 + {
  2502 + CGPoint locationPoint = [self mapPositionForAnnotation:userLocation];
  2503 +
  2504 + CGRect locationRect = CGRectMake(locationPoint.x - userLocation.location.horizontalAccuracy,
  2505 + locationPoint.y - userLocation.location.horizontalAccuracy,
  2506 + userLocation.location.horizontalAccuracy * 2,
  2507 + userLocation.location.horizontalAccuracy * 2);
  2508 +
  2509 + return CGRectIntersectsRect([self bounds], locationRect);
  2510 + }
  2511 +
  2512 + return NO;
  2513 +}
  2514 +
  2515 +- (void)setUserTrackingMode:(RMUserTrackingMode)mode
  2516 +{
  2517 + [self setUserTrackingMode:mode animated:YES];
  2518 +}
  2519 +
  2520 +- (void)setUserTrackingMode:(RMUserTrackingMode)mode animated:(BOOL)animated
  2521 +{
  2522 + if (mode == userTrackingMode)
  2523 + return;
  2524 +
  2525 + userTrackingMode = mode;
  2526 +
  2527 + switch (userTrackingMode)
  2528 + {
  2529 + case RMUserTrackingModeNone:
  2530 + default:
  2531 + {
  2532 + [locationManager stopUpdatingHeading];
  2533 +
  2534 + [UIView animateWithDuration:(animated ? 0.5 : 0.0)
  2535 + delay:0.0
  2536 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
  2537 + animations:^(void)
  2538 + {
  2539 + _mapScrollView.transform = CGAffineTransformIdentity;
  2540 + _overlayView.transform = CGAffineTransformIdentity;
  2541 +
  2542 + for (RMAnnotation *annotation in _annotations)
  2543 + if ( ! annotation.isUserLocationAnnotation)
  2544 + annotation.layer.transform = CATransform3DIdentity;
  2545 + }
  2546 + completion:nil];
  2547 +
  2548 + if (userLocationTrackingView || userHeadingTrackingView)
  2549 + {
  2550 + [userLocationTrackingView removeFromSuperview];
  2551 + userLocationTrackingView = nil;
  2552 + [userHeadingTrackingView removeFromSuperview];
  2553 + userHeadingTrackingView = nil;
  2554 + }
  2555 +
  2556 + userLocation.layer.hidden = NO;
  2557 +
  2558 + break;
  2559 + }
  2560 + case RMUserTrackingModeFollow:
  2561 + {
  2562 + self.showsUserLocation = YES;
  2563 +
  2564 + [locationManager stopUpdatingHeading];
  2565 +
  2566 + if (self.userLocation)
  2567 + [self locationManager:locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location];
  2568 +
  2569 + if (userLocationTrackingView || userHeadingTrackingView)
  2570 + {
  2571 + [userLocationTrackingView removeFromSuperview];
  2572 + userLocationTrackingView = nil;
  2573 + [userHeadingTrackingView removeFromSuperview];
  2574 + userHeadingTrackingView = nil;
  2575 + }
  2576 +
  2577 + [UIView animateWithDuration:(animated ? 0.5 : 0.0)
  2578 + delay:0.0
  2579 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
  2580 + animations:^(void)
  2581 + {
  2582 + _mapScrollView.transform = CGAffineTransformIdentity;
  2583 + _overlayView.transform = CGAffineTransformIdentity;
  2584 +
  2585 + for (RMAnnotation *annotation in _annotations)
  2586 + if ( ! annotation.isUserLocationAnnotation)
  2587 + annotation.layer.transform = CATransform3DIdentity;
  2588 + }
  2589 + completion:nil];
  2590 +
  2591 + userLocation.layer.hidden = NO;
  2592 +
  2593 + break;
  2594 + }
  2595 + case RMUserTrackingModeFollowWithHeading:
2364 { 2596 {
2365 - // Remove the layer from the screen  
2366 - annotation.layer = nil; 2597 + self.showsUserLocation = YES;
  2598 +
  2599 + userLocation.layer.hidden = YES;
  2600 +
  2601 + userHeadingTrackingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"HeadingAngleSmall.png"]];
  2602 +
  2603 + userHeadingTrackingView.center = CGPointMake(round([self bounds].size.width / 2),
  2604 + round([self bounds].size.height / 2) - (userHeadingTrackingView.bounds.size.height / 2) - 4);
  2605 +
  2606 + userHeadingTrackingView.alpha = 0.0;
  2607 +
  2608 + [self addSubview:userHeadingTrackingView];
  2609 +
  2610 + [UIView animateWithDuration:0.5 animations:^(void) { userHeadingTrackingView.alpha = 1.0; }];
  2611 +
  2612 + userLocationTrackingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"TrackingDot.png"]];
  2613 +
  2614 + userLocationTrackingView.center = CGPointMake(round([self bounds].size.width / 2),
  2615 + round([self bounds].size.height / 2));
  2616 +
  2617 + [self addSubview:userLocationTrackingView];
  2618 +
  2619 + if (self.zoom < 3)
  2620 + [self zoomByFactor:exp2f(3 - [self zoom]) near:self.center animated:YES];
  2621 +
  2622 + if (self.userLocation)
  2623 + [self locationManager:locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location];
  2624 +
  2625 + [locationManager startUpdatingHeading];
  2626 +
  2627 + break;
2367 } 2628 }
2368 } 2629 }
2369 2630
2370 - [_annotations removeAllObjects];  
2371 - [_visibleAnnotations removeAllObjects];  
2372 - [self.quadTree removeAllObjects];  
2373 - [self correctPositionOfAllAnnotations]; 2631 + if (_delegateHasDidChangeUserTrackingMode)
  2632 + [_delegate mapView:self didChangeUserTrackingMode:userTrackingMode animated:animated];
2374 } 2633 }
2375 2634
2376 -- (CGPoint)mapPositionForAnnotation:(RMAnnotation *)annotation 2635 +- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
2377 { 2636 {
2378 - [self correctScreenPosition:annotation animated:NO];  
2379 - return annotation.position; 2637 + if ( ! showsUserLocation || _mapScrollView.isDragging)
  2638 + return;
  2639 +
  2640 + if ([newLocation distanceFromLocation:oldLocation])
  2641 + {
  2642 + userLocation.location = newLocation;
  2643 +
  2644 + if (_delegateHasDidUpdateUserLocation)
  2645 + [_delegate mapView:self didUpdateUserLocation:userLocation];
  2646 + }
  2647 +
  2648 + if (self.userTrackingMode != RMUserTrackingModeNone)
  2649 + {
  2650 + // zoom centered on user location unless we're already centered there (or very close)
  2651 + //
  2652 + CGPoint mapCenterPoint = [self convertPoint:self.center fromView:self.superview];
  2653 + CGPoint userLocationPoint = [self mapPositionForAnnotation:userLocation];
  2654 +
  2655 + if (fabsf(userLocationPoint.x - mapCenterPoint.x) > 2 || fabsf(userLocationPoint.y - mapCenterPoint.y > 2))
  2656 + {
  2657 + float delta = newLocation.horizontalAccuracy / 110000; // approx. meter per degree latitude
  2658 +
  2659 + CLLocationCoordinate2D southWest = CLLocationCoordinate2DMake(newLocation.coordinate.latitude - delta,
  2660 + newLocation.coordinate.longitude - delta);
  2661 +
  2662 + CLLocationCoordinate2D northEast = CLLocationCoordinate2DMake(newLocation.coordinate.latitude + delta,
  2663 + newLocation.coordinate.longitude + delta);
  2664 +
  2665 + if (northEast.latitude != [self latitudeLongitudeBoundingBox].northEast.latitude ||
  2666 + northEast.longitude != [self latitudeLongitudeBoundingBox].northEast.longitude ||
  2667 + southWest.latitude != [self latitudeLongitudeBoundingBox].southWest.latitude ||
  2668 + southWest.longitude != [self latitudeLongitudeBoundingBox].southWest.longitude)
  2669 + [self zoomWithLatitudeLongitudeBoundsSouthWest:southWest northEast:northEast animated:YES];
  2670 + }
  2671 + }
  2672 +
  2673 + RMAnnotation *accuracyCircleAnnotation = nil;
  2674 +
  2675 + for (RMAnnotation *annotation in _annotations)
  2676 + if ([annotation.annotationType isEqualToString:kRMAccuracyCircleAnnotationTypeName])
  2677 + accuracyCircleAnnotation = annotation;
  2678 +
  2679 + if ( ! accuracyCircleAnnotation)
  2680 + {
  2681 + accuracyCircleAnnotation = [RMAnnotation annotationWithMapView:self coordinate:newLocation.coordinate andTitle:nil];
  2682 +
  2683 + accuracyCircleAnnotation.annotationType = kRMAccuracyCircleAnnotationTypeName;
  2684 +
  2685 + accuracyCircleAnnotation.clusteringEnabled = NO;
  2686 +
  2687 + accuracyCircleAnnotation.layer = [[RMCircle alloc] initWithView:self radiusInMeters:newLocation.horizontalAccuracy];
  2688 +
  2689 + accuracyCircleAnnotation.isUserLocationAnnotation = YES;
  2690 +
  2691 + ((RMCircle *)accuracyCircleAnnotation.layer).lineColor = [UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.7];
  2692 + ((RMCircle *)accuracyCircleAnnotation.layer).fillColor = [UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.15];
  2693 +
  2694 + ((RMCircle *)accuracyCircleAnnotation.layer).lineWidthInPixels = 2.0;
  2695 +
  2696 + [self addAnnotation:accuracyCircleAnnotation];
  2697 + }
  2698 +
  2699 + if ([newLocation distanceFromLocation:oldLocation])
  2700 + accuracyCircleAnnotation.coordinate = newLocation.coordinate;
  2701 +
  2702 + if (newLocation.horizontalAccuracy != oldLocation.horizontalAccuracy)
  2703 + ((RMCircle *)accuracyCircleAnnotation.layer).radiusInMeters = newLocation.horizontalAccuracy;
  2704 +
  2705 + RMAnnotation *trackingHaloAnnotation = nil;
  2706 +
  2707 + for (RMAnnotation *annotation in _annotations)
  2708 + if ([annotation.annotationType isEqualToString:kRMTrackingHaloAnnotationTypeName])
  2709 + trackingHaloAnnotation = annotation;
  2710 +
  2711 + if ( ! trackingHaloAnnotation)
  2712 + {
  2713 + trackingHaloAnnotation = [RMAnnotation annotationWithMapView:self coordinate:newLocation.coordinate andTitle:nil];
  2714 +
  2715 + trackingHaloAnnotation.annotationType = kRMTrackingHaloAnnotationTypeName;
  2716 +
  2717 + trackingHaloAnnotation.clusteringEnabled = NO;
  2718 +
  2719 + // create image marker
  2720 + //
  2721 + trackingHaloAnnotation.layer = [[RMMarker alloc] initWithUIImage:[UIImage imageNamed:@"TrackingDotHalo.png"]];
  2722 +
  2723 + trackingHaloAnnotation.isUserLocationAnnotation = YES;
  2724 +
  2725 + [CATransaction begin];
  2726 + [CATransaction setAnimationDuration:2.5];
  2727 + [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
  2728 +
  2729 + // scale out radially
  2730 + //
  2731 + CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
  2732 +
  2733 + boundsAnimation.repeatCount = MAXFLOAT;
  2734 +
  2735 + boundsAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
  2736 + boundsAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)];
  2737 +
  2738 + boundsAnimation.removedOnCompletion = NO;
  2739 +
  2740 + boundsAnimation.fillMode = kCAFillModeForwards;
  2741 +
  2742 + [trackingHaloAnnotation.layer addAnimation:boundsAnimation forKey:@"animateScale"];
  2743 +
  2744 + // go transparent as scaled out
  2745 + //
  2746 + CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
  2747 +
  2748 + opacityAnimation.repeatCount = MAXFLOAT;
  2749 +
  2750 + opacityAnimation.fromValue = [NSNumber numberWithFloat:1.0];
  2751 + opacityAnimation.toValue = [NSNumber numberWithFloat:-1.0];
  2752 +
  2753 + opacityAnimation.removedOnCompletion = NO;
  2754 +
  2755 + opacityAnimation.fillMode = kCAFillModeForwards;
  2756 +
  2757 + [trackingHaloAnnotation.layer addAnimation:opacityAnimation forKey:@"animateOpacity"];
  2758 +
  2759 + [CATransaction commit];
  2760 +
  2761 + [self addAnnotation:trackingHaloAnnotation];
  2762 + }
  2763 +
  2764 + if ([newLocation distanceFromLocation:oldLocation])
  2765 + trackingHaloAnnotation.coordinate = newLocation.coordinate;
  2766 +
  2767 + userLocation.layer.hidden = ((trackingHaloAnnotation.coordinate.latitude == 0 && trackingHaloAnnotation.coordinate.longitude == 0) || self.userTrackingMode == RMUserTrackingModeFollowWithHeading);
  2768 +
  2769 + accuracyCircleAnnotation.layer.hidden = newLocation.horizontalAccuracy <= 10;
  2770 +
  2771 + trackingHaloAnnotation.layer.hidden = ((trackingHaloAnnotation.coordinate.latitude == 0 && trackingHaloAnnotation.coordinate.longitude == 0) || newLocation.horizontalAccuracy > 10);
  2772 +
  2773 + if ( ! [_annotations containsObject:userLocation])
  2774 + [self addAnnotation:userLocation];
  2775 +}
  2776 +
  2777 +- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager
  2778 +{
  2779 + return YES;
  2780 +}
  2781 +
  2782 +- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
  2783 +{
  2784 + if ( ! showsUserLocation || _mapScrollView.isDragging)
  2785 + return;
  2786 +
  2787 + userLocation.heading = newHeading;
  2788 +
  2789 + if (_delegateHasDidUpdateUserLocation)
  2790 + [_delegate mapView:self didUpdateUserLocation:userLocation];
  2791 +
  2792 + if (newHeading.trueHeading != 0 && self.userTrackingMode == RMUserTrackingModeFollowWithHeading)
  2793 + {
  2794 + [CATransaction begin];
  2795 + [CATransaction setAnimationDuration:1.0];
  2796 + [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
  2797 +
  2798 + [UIView animateWithDuration:1.0
  2799 + delay:0.0
  2800 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
  2801 + animations:^(void)
  2802 + {
  2803 + CGFloat angle = (M_PI / -180) * newHeading.trueHeading;
  2804 +
  2805 + _mapScrollView.transform = CGAffineTransformMakeRotation(angle);
  2806 + _overlayView.transform = CGAffineTransformMakeRotation(angle);
  2807 +
  2808 + for (RMAnnotation *annotation in _annotations)
  2809 + if ( ! annotation.isUserLocationAnnotation)
  2810 + annotation.layer.transform = CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(-angle));
  2811 + }
  2812 + completion:nil];
  2813 +
  2814 + [CATransaction commit];
  2815 + }
  2816 +}
  2817 +
  2818 +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
  2819 +{
  2820 + if ([error code] != kCLErrorLocationUnknown)
  2821 + {
  2822 + self.userTrackingMode = RMUserTrackingModeNone;
  2823 +
  2824 + if (_delegateHasDidFailToLocateUserWithError)
  2825 + [_delegate mapView:self didFailToLocateUserWithError:error];
  2826 + }
2380 } 2827 }
2381 2828
2382 @end 2829 @end
@@ -29,6 +29,13 @@ @@ -29,6 +29,13 @@
29 @class RMMapLayer; 29 @class RMMapLayer;
30 @class RMMarker; 30 @class RMMarker;
31 @class RMAnnotation; 31 @class RMAnnotation;
  32 +@class RMUserLocation;
  33 +
  34 +typedef enum {
  35 + RMUserTrackingModeNone = 0,
  36 + RMUserTrackingModeFollow = 1,
  37 + RMUserTrackingModeFollowWithHeading = 2
  38 +} RMUserTrackingMode;
32 39
33 // Use this for notifications of map panning, zooming, and taps on the RMMapView. 40 // Use this for notifications of map panning, zooming, and taps on the RMMapView.
34 @protocol RMMapViewDelegate <NSObject> 41 @protocol RMMapViewDelegate <NSObject>
@@ -67,4 +74,10 @@ @@ -67,4 +74,10 @@
67 - (void)mapView:(RMMapView *)map didDragAnnotation:(RMAnnotation *)annotation withDelta:(CGPoint)delta; 74 - (void)mapView:(RMMapView *)map didDragAnnotation:(RMAnnotation *)annotation withDelta:(CGPoint)delta;
68 - (void)mapView:(RMMapView *)map didEndDragAnnotation:(RMAnnotation *)annotation; 75 - (void)mapView:(RMMapView *)map didEndDragAnnotation:(RMAnnotation *)annotation;
69 76
  77 +- (void)mapViewWillStartLocatingUser:(RMMapView *)mapView;
  78 +- (void)mapViewDidStopLocatingUser:(RMMapView *)mapView;
  79 +- (void)mapView:(RMMapView *)mapView didUpdateUserLocation:(RMUserLocation *)userLocation;
  80 +- (void)mapView:(RMMapView *)mapView didFailToLocateUserWithError:(NSError *)error;
  81 +- (void)mapView:(RMMapView *)mapView didChangeUserTrackingMode:(RMUserTrackingMode)mode animated:(BOOL)animated;
  82 +
70 @end 83 @end
  1 +//
  2 +// RMUserLocation.h
  3 +// MapView
  4 +//
  5 +// Created by Justin Miller on 5/8/12.
  6 +// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
  7 +//
  8 +
  9 +#import "RMAnnotation.h"
  10 +
  11 +@interface RMUserLocation : RMAnnotation
  12 +
  13 +@property (nonatomic, readonly, getter=isUpdating) BOOL updating;
  14 +@property (nonatomic, readonly, retain) CLLocation *location;
  15 +@property (nonatomic, readonly, retain) CLHeading *heading;
  16 +
  17 +@end
  1 +//
  2 +// RMUserLocation.m
  3 +// MapView
  4 +//
  5 +// Created by Justin Miller on 5/8/12.
  6 +// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
  7 +//
  8 +
  9 +#import "RMUserLocation.h"
  10 +#import "RMMarker.h"
  11 +#import "RMMapView.h"
  12 +
  13 +@implementation RMUserLocation
  14 +
  15 +@synthesize updating;
  16 +@synthesize location;
  17 +@synthesize heading;
  18 +
  19 +- (id)initWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle
  20 +{
  21 + if ( ! (self = [super initWithMapView:aMapView coordinate:aCoordinate andTitle:aTitle]))
  22 + return nil;
  23 +
  24 + NSAssert([[NSBundle mainBundle] pathForResource:@"TrackingDot" ofType:@"png"], @"Unable to find necessary user location graphical assets (copy from MapView/Map/Resources)");
  25 +
  26 + layer = [[RMMarker alloc] initWithUIImage:[UIImage imageNamed:@"TrackingDot.png"]];
  27 +
  28 + annotationType = [kRMUserLocationAnnotationTypeName retain];
  29 +
  30 + clusteringEnabled = NO;
  31 +
  32 + return self;
  33 +}
  34 +
  35 +- (void)dealloc
  36 +{
  37 + [layer release]; layer = nil;
  38 + [annotationType release]; annotationType = nil;
  39 + [location release]; location = nil;
  40 + [heading release]; heading = nil;
  41 + [super dealloc];
  42 +}
  43 +
  44 +- (BOOL)isUpdating
  45 +{
  46 + return (self.mapView.userTrackingMode != RMUserTrackingModeNone);
  47 +}
  48 +
  49 +- (void)setLocation:(CLLocation *)newLocation
  50 +{
  51 + if ([newLocation distanceFromLocation:location] && newLocation.coordinate.latitude != 0 && newLocation.coordinate.longitude != 0)
  52 + {
  53 + [self willChangeValueForKey:@"location"];
  54 + [location release];
  55 + location = [newLocation retain];
  56 + self.coordinate = location.coordinate;
  57 + [self didChangeValueForKey:@"location"];
  58 + }
  59 +}
  60 +
  61 +- (void)setHeading:(CLHeading *)newHeading
  62 +{
  63 + if (newHeading.trueHeading != heading.trueHeading)
  64 + {
  65 + [self willChangeValueForKey:@"heading"];
  66 + [heading release];
  67 + heading = [newHeading retain];
  68 + [self didChangeValueForKey:@"heading"];
  69 + }
  70 +}
  71 +
  72 +- (BOOL)isUserLocationAnnotation
  73 +{
  74 + return YES;
  75 +}
  76 +
  77 +@end
  1 +//
  2 +// RMUserTrackingBarButtonItem.h
  3 +// MapView
  4 +//
  5 +// Created by Justin Miller on 5/10/12.
  6 +// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
  7 +//
  8 +
  9 +#import <UIKit/UIKit.h>
  10 +
  11 +@class RMMapView;
  12 +
  13 +@interface RMUserTrackingBarButtonItem : UIBarButtonItem
  14 +
  15 +- (id)initWithMapView:(RMMapView *)mapView;
  16 +
  17 +@property (nonatomic, retain) RMMapView *mapView;
  18 +
  19 +@end
  1 +//
  2 +// RMUserTrackingBarButtonItem.m
  3 +// MapView
  4 +//
  5 +// Created by Justin Miller on 5/10/12.
  6 +// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
  7 +//
  8 +
  9 +#import "RMUserTrackingBarButtonItem.h"
  10 +
  11 +#import "RMMapView.h"
  12 +#import "RMUserLocation.h"
  13 +
  14 +typedef enum {
  15 + RMUserTrackingButtonStateActivity = 0,
  16 + RMUserTrackingButtonStateLocation = 1,
  17 + RMUserTrackingButtonStateHeading = 2
  18 +} RMUserTrackingButtonState;
  19 +
  20 +@interface RMUserTrackingBarButtonItem ()
  21 +
  22 +@property (nonatomic, retain) UISegmentedControl *segmentedControl;
  23 +@property (nonatomic, retain) UIImageView *buttonImageView;
  24 +@property (nonatomic, retain) UIActivityIndicatorView *activityView;
  25 +@property (nonatomic) RMUserTrackingButtonState state;
  26 +
  27 +- (void)updateAppearance;
  28 +- (void)changeMode:(id)sender;
  29 +
  30 +@end
  31 +
  32 +#pragma mark -
  33 +
  34 +@implementation RMUserTrackingBarButtonItem
  35 +
  36 +@synthesize mapView=_mapView;
  37 +@synthesize segmentedControl;
  38 +@synthesize buttonImageView;
  39 +@synthesize activityView;
  40 +@synthesize state;
  41 +
  42 +- (id)initWithMapView:(RMMapView *)mapView
  43 +{
  44 + if (!(self = [super initWithCustomView:[[UIControl alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]]))
  45 + return nil;
  46 +
  47 + segmentedControl = [[[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObject:@""]] retain];
  48 + segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
  49 + [segmentedControl setWidth:32.0 forSegmentAtIndex:0];
  50 + segmentedControl.userInteractionEnabled = NO;
  51 + segmentedControl.tintColor = self.tintColor;
  52 + segmentedControl.center = self.customView.center;
  53 +
  54 + [self.customView addSubview:segmentedControl];
  55 +
  56 + buttonImageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"TrackingLocation.png"]] retain];
  57 + buttonImageView.contentMode = UIViewContentModeCenter;
  58 + buttonImageView.frame = CGRectMake(0, 0, 32, 32);
  59 + buttonImageView.center = self.customView.center;
  60 + buttonImageView.userInteractionEnabled = NO;
  61 +
  62 + [self.customView addSubview:buttonImageView];
  63 +
  64 + activityView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite] retain];
  65 + activityView.hidesWhenStopped = YES;
  66 + activityView.center = self.customView.center;
  67 + activityView.userInteractionEnabled = NO;
  68 +
  69 + [self.customView addSubview:activityView];
  70 +
  71 + [((UIControl *)self.customView) addTarget:self action:@selector(changeMode:) forControlEvents:UIControlEventTouchUpInside];
  72 +
  73 + _mapView = [mapView retain];
  74 +
  75 + [_mapView addObserver:self forKeyPath:@"userTrackingMode" options:NSKeyValueObservingOptionNew context:nil];
  76 + [_mapView addObserver:self forKeyPath:@"userLocation.location" options:NSKeyValueObservingOptionNew context:nil];
  77 +
  78 + state = RMUserTrackingButtonStateLocation;
  79 +
  80 + [self updateAppearance];
  81 +
  82 + return self;
  83 +}
  84 +
  85 +- (void)dealloc
  86 +{
  87 + [segmentedControl release]; segmentedControl = nil;
  88 + [buttonImageView release]; buttonImageView = nil;
  89 + [activityView release]; activityView = nil;
  90 + [_mapView removeObserver:self forKeyPath:@"userTrackingMode"];
  91 + [_mapView removeObserver:self forKeyPath:@"userLocation.location"];
  92 + [_mapView release]; _mapView = nil;
  93 +
  94 + [super dealloc];
  95 +}
  96 +
  97 +#pragma mark -
  98 +
  99 +- (void)setMapView:(RMMapView *)newMapView
  100 +{
  101 + if ( ! [newMapView isEqual:_mapView])
  102 + {
  103 + [_mapView removeObserver:self forKeyPath:@"userTrackingMode"];
  104 + [_mapView removeObserver:self forKeyPath:@"userLocation.location"];
  105 + [_mapView release];
  106 +
  107 + _mapView = [newMapView retain];
  108 + [_mapView addObserver:self forKeyPath:@"userTrackingMode" options:NSKeyValueObservingOptionNew context:nil];
  109 + [_mapView addObserver:self forKeyPath:@"userLocation.location" options:NSKeyValueObservingOptionNew context:nil];
  110 +
  111 + [self updateAppearance];
  112 + }
  113 +}
  114 +
  115 +- (void)setTintColor:(UIColor *)newTintColor
  116 +{
  117 + [super setTintColor:newTintColor];
  118 +
  119 + segmentedControl.tintColor = newTintColor;
  120 +}
  121 +
  122 +#pragma mark -
  123 +
  124 +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  125 +{
  126 + [self updateAppearance];
  127 +}
  128 +
  129 +#pragma mark -
  130 +
  131 +- (void)updateAppearance
  132 +{
  133 + // "selection" state
  134 + //
  135 + segmentedControl.selectedSegmentIndex = (_mapView.userTrackingMode == RMUserTrackingModeNone ? UISegmentedControlNoSegment : 0);
  136 +
  137 + // activity/image state
  138 + //
  139 + if (_mapView.userTrackingMode != RMUserTrackingModeNone && ( ! _mapView.userLocation || ! _mapView.userLocation.location || (_mapView.userLocation.location.coordinate.latitude == 0 && _mapView.userLocation.location.coordinate.longitude == 0)))
  140 + {
  141 + // if we should be tracking but don't yet have a location, show activity
  142 + //
  143 + [UIView animateWithDuration:0.25
  144 + delay:0.0
  145 + options:UIViewAnimationOptionBeginFromCurrentState
  146 + animations:^(void)
  147 + {
  148 + buttonImageView.transform = CGAffineTransformMakeScale(0.01, 0.01);
  149 + activityView.transform = CGAffineTransformMakeScale(0.01, 0.01);
  150 + }
  151 + completion:^(BOOL finished)
  152 + {
  153 + buttonImageView.hidden = YES;
  154 +
  155 + [activityView startAnimating];
  156 +
  157 + [UIView animateWithDuration:0.25 animations:^(void)
  158 + {
  159 + buttonImageView.transform = CGAffineTransformIdentity;
  160 + activityView.transform = CGAffineTransformIdentity;
  161 + }];
  162 + }];
  163 +
  164 + state = RMUserTrackingButtonStateActivity;
  165 + }
  166 + else
  167 + {
  168 + if ((_mapView.userTrackingMode != RMUserTrackingModeFollowWithHeading && state != RMUserTrackingButtonStateLocation) ||
  169 + (_mapView.userTrackingMode == RMUserTrackingModeFollowWithHeading && state != RMUserTrackingButtonStateHeading))
  170 + {
  171 + // if image state doesn't match mode, update it
  172 + //
  173 + [UIView animateWithDuration:0.25
  174 + delay:0.0
  175 + options:UIViewAnimationOptionBeginFromCurrentState
  176 + animations:^(void)
  177 + {
  178 + buttonImageView.transform = CGAffineTransformMakeScale(0.01, 0.01);
  179 + activityView.transform = CGAffineTransformMakeScale(0.01, 0.01);
  180 + }
  181 + completion:^(BOOL finished)
  182 + {
  183 + buttonImageView.image = [UIImage imageNamed:(_mapView.userTrackingMode == RMUserTrackingModeFollowWithHeading ? @"TrackingHeading.png" : @"TrackingLocation.png")];
  184 + buttonImageView.hidden = NO;
  185 +
  186 + [activityView stopAnimating];
  187 +
  188 + [UIView animateWithDuration:0.25 animations:^(void)
  189 + {
  190 + buttonImageView.transform = CGAffineTransformIdentity;
  191 + activityView.transform = CGAffineTransformIdentity;
  192 + }];
  193 + }];
  194 +
  195 + state = (_mapView.userTrackingMode == RMUserTrackingModeFollowWithHeading ? RMUserTrackingButtonStateHeading : RMUserTrackingButtonStateLocation);
  196 + }
  197 + }
  198 +}
  199 +
  200 +- (void)changeMode:(id)sender
  201 +{
  202 + if (_mapView)
  203 + {
  204 + switch (_mapView.userTrackingMode)
  205 + {
  206 + case RMUserTrackingModeNone:
  207 + default:
  208 + {
  209 + _mapView.userTrackingMode = RMUserTrackingModeFollow;
  210 +
  211 + break;
  212 + }
  213 + case RMUserTrackingModeFollow:
  214 + {
  215 + if ([CLLocationManager headingAvailable])
  216 + _mapView.userTrackingMode = RMUserTrackingModeFollowWithHeading;
  217 + else
  218 + _mapView.userTrackingMode = RMUserTrackingModeNone;
  219 +
  220 + break;
  221 + }
  222 + case RMUserTrackingModeFollowWithHeading:
  223 + {
  224 + _mapView.userTrackingMode = RMUserTrackingModeNone;
  225 +
  226 + break;
  227 + }
  228 + }
  229 + }
  230 +
  231 + [self updateAppearance];
  232 +}
  233 +
  234 +@end
@@ -69,3 +69,5 @@ @@ -69,3 +69,5 @@
69 #import "RMTileImage.h" 69 #import "RMTileImage.h"
70 #import "RMTileSource.h" 70 #import "RMTileSource.h"
71 #import "RMTileSourcesContainer.h" 71 #import "RMTileSourcesContainer.h"
  72 +#import "RMUserLocation.h"
  73 +#import "RMUserTrackingBarButtonItem.h"
@@ -101,6 +101,10 @@ @@ -101,6 +101,10 @@
101 DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */; }; 101 DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */; };
102 DD98B6FA14D76B930092882F /* RMMapBoxSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DD98B6F814D76B930092882F /* RMMapBoxSource.h */; }; 102 DD98B6FA14D76B930092882F /* RMMapBoxSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DD98B6F814D76B930092882F /* RMMapBoxSource.h */; };
103 DD98B6FB14D76B930092882F /* RMMapBoxSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD98B6F914D76B930092882F /* RMMapBoxSource.m */; }; 103 DD98B6FB14D76B930092882F /* RMMapBoxSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD98B6F914D76B930092882F /* RMMapBoxSource.m */; };
  104 + DD8FD7541559E4A40044D96F /* RMUserLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8FD7521559E4A40044D96F /* RMUserLocation.h */; };
  105 + DD8FD7551559E4A40044D96F /* RMUserLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8FD7531559E4A40044D96F /* RMUserLocation.m */; };
  106 + DDA6B8BD155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */; };
  107 + DDA6B8BE155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */; };
104 /* End PBXBuildFile section */ 108 /* End PBXBuildFile section */
105 109
106 /* Begin PBXContainerItemProxy section */ 110 /* Begin PBXContainerItemProxy section */
@@ -230,6 +234,20 @@ @@ -230,6 +234,20 @@
230 DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapQuestOSMSource.m; sourceTree = "<group>"; }; 234 DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapQuestOSMSource.m; sourceTree = "<group>"; };
231 DD98B6F814D76B930092882F /* RMMapBoxSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapBoxSource.h; sourceTree = "<group>"; }; 235 DD98B6F814D76B930092882F /* RMMapBoxSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapBoxSource.h; sourceTree = "<group>"; };
232 DD98B6F914D76B930092882F /* RMMapBoxSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapBoxSource.m; sourceTree = "<group>"; }; 236 DD98B6F914D76B930092882F /* RMMapBoxSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapBoxSource.m; sourceTree = "<group>"; };
  237 + DD8FD7521559E4A40044D96F /* RMUserLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMUserLocation.h; sourceTree = "<group>"; };
  238 + DD8FD7531559E4A40044D96F /* RMUserLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMUserLocation.m; sourceTree = "<group>"; };
  239 + DD8FD7631559EE120044D96F /* HeadingAngleSmall.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = HeadingAngleSmall.png; path = Resources/HeadingAngleSmall.png; sourceTree = "<group>"; };
  240 + DD8FD7641559EE120044D96F /* HeadingAngleSmall@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "HeadingAngleSmall@2x.png"; path = "Resources/HeadingAngleSmall@2x.png"; sourceTree = "<group>"; };
  241 + DD8FD7651559EE120044D96F /* TrackingDot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingDot.png; path = Resources/TrackingDot.png; sourceTree = "<group>"; };
  242 + DD8FD7661559EE120044D96F /* TrackingDot@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingDot@2x.png"; path = "Resources/TrackingDot@2x.png"; sourceTree = "<group>"; };
  243 + DD8FD7691559EE120044D96F /* TrackingDotHalo@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingDotHalo@2x.png"; path = "Resources/TrackingDotHalo@2x.png"; sourceTree = "<group>"; };
  244 + DD8FD76C1559EE120044D96F /* TrackingDotHalo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingDotHalo.png; path = Resources/TrackingDotHalo.png; sourceTree = "<group>"; };
  245 + DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMUserTrackingBarButtonItem.h; sourceTree = "<group>"; };
  246 + DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMUserTrackingBarButtonItem.m; sourceTree = "<group>"; };
  247 + DDA6B8C0155CAB9A003DB5D8 /* TrackingLocation.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingLocation.png; path = Resources/TrackingLocation.png; sourceTree = "<group>"; };
  248 + DDA6B8C1155CAB9A003DB5D8 /* TrackingLocation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingLocation@2x.png"; path = "Resources/TrackingLocation@2x.png"; sourceTree = "<group>"; };
  249 + DDA6B8C2155CAB9A003DB5D8 /* TrackingHeading.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingHeading.png; path = Resources/TrackingHeading.png; sourceTree = "<group>"; };
  250 + DDA6B8C3155CAB9A003DB5D8 /* TrackingHeading@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingHeading@2x.png"; path = "Resources/TrackingHeading@2x.png"; sourceTree = "<group>"; };
233 /* End PBXFileReference section */ 251 /* End PBXFileReference section */
234 252
235 /* Begin PBXFrameworksBuildPhase section */ 253 /* Begin PBXFrameworksBuildPhase section */
@@ -476,6 +494,7 @@ @@ -476,6 +494,7 @@
476 B83E64EB0E80E73F001663B6 /* Tile Source */, 494 B83E64EB0E80E73F001663B6 /* Tile Source */,
477 B83E64CE0E80E73F001663B6 /* Tile Layer & Overlay */, 495 B83E64CE0E80E73F001663B6 /* Tile Layer & Overlay */,
478 B86F26A80E8742ED007A3773 /* Markers and other layers */, 496 B86F26A80E8742ED007A3773 /* Markers and other layers */,
  497 + DD8FD7571559ED930044D96F /* User Location */,
479 1266929E0EB75BEA00E002D5 /* Configuration */, 498 1266929E0EB75BEA00E002D5 /* Configuration */,
480 B8474B8C0EB40094006A0BC1 /* FMDB */, 499 B8474B8C0EB40094006A0BC1 /* FMDB */,
481 ); 500 );
@@ -495,6 +514,35 @@ @@ -495,6 +514,35 @@
495 name = "Coordinate Systems"; 514 name = "Coordinate Systems";
496 sourceTree = "<group>"; 515 sourceTree = "<group>";
497 }; 516 };
  517 + DD8FD7571559ED930044D96F /* User Location */ = {
  518 + isa = PBXGroup;
  519 + children = (
  520 + DD8FD7521559E4A40044D96F /* RMUserLocation.h */,
  521 + DD8FD7531559E4A40044D96F /* RMUserLocation.m */,
  522 + DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */,
  523 + DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */,
  524 + DD8FD7581559EDA80044D96F /* Resources */,
  525 + );
  526 + name = "User Location";
  527 + sourceTree = "<group>";
  528 + };
  529 + DD8FD7581559EDA80044D96F /* Resources */ = {
  530 + isa = PBXGroup;
  531 + children = (
  532 + DD8FD7631559EE120044D96F /* HeadingAngleSmall.png */,
  533 + DD8FD7641559EE120044D96F /* HeadingAngleSmall@2x.png */,
  534 + DD8FD7651559EE120044D96F /* TrackingDot.png */,
  535 + DD8FD7661559EE120044D96F /* TrackingDot@2x.png */,
  536 + DD8FD76C1559EE120044D96F /* TrackingDotHalo.png */,
  537 + DD8FD7691559EE120044D96F /* TrackingDotHalo@2x.png */,
  538 + DDA6B8C2155CAB9A003DB5D8 /* TrackingHeading.png */,
  539 + DDA6B8C3155CAB9A003DB5D8 /* TrackingHeading@2x.png */,
  540 + DDA6B8C0155CAB9A003DB5D8 /* TrackingLocation.png */,
  541 + DDA6B8C1155CAB9A003DB5D8 /* TrackingLocation@2x.png */,
  542 + );
  543 + name = Resources;
  544 + sourceTree = "<group>";
  545 + };
498 /* End PBXGroup section */ 546 /* End PBXGroup section */
499 547
500 /* Begin PBXHeadersBuildPhase section */ 548 /* Begin PBXHeadersBuildPhase section */
@@ -543,6 +591,8 @@ @@ -543,6 +591,8 @@
543 DD8CDB4A14E0507100B73EB9 /* RMMapQuestOSMSource.h in Headers */, 591 DD8CDB4A14E0507100B73EB9 /* RMMapQuestOSMSource.h in Headers */,
544 1607499514E120A100D535F5 /* RMGenericMapSource.h in Headers */, 592 1607499514E120A100D535F5 /* RMGenericMapSource.h in Headers */,
545 16FFF2CB14E3DBF700A170EC /* RMMapQuestOpenAerialSource.h in Headers */, 593 16FFF2CB14E3DBF700A170EC /* RMMapQuestOpenAerialSource.h in Headers */,
  594 + DD8FD7541559E4A40044D96F /* RMUserLocation.h in Headers */,
  595 + DDA6B8BD155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h in Headers */,
546 16F3581B15864135003A3AD9 /* RMMapScrollView.h in Headers */, 596 16F3581B15864135003A3AD9 /* RMMapScrollView.h in Headers */,
547 16F98C961590CFF000FF90CE /* RMShape.h in Headers */, 597 16F98C961590CFF000FF90CE /* RMShape.h in Headers */,
548 16FBF07615936BF1004ECAD1 /* RMTileSourcesContainer.h in Headers */, 598 16FBF07615936BF1004ECAD1 /* RMTileSourcesContainer.h in Headers */,
@@ -656,6 +706,8 @@ @@ -656,6 +706,8 @@
656 DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */, 706 DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */,
657 1607499614E120A100D535F5 /* RMGenericMapSource.m in Sources */, 707 1607499614E120A100D535F5 /* RMGenericMapSource.m in Sources */,
658 16FFF2CC14E3DBF700A170EC /* RMMapQuestOpenAerialSource.m in Sources */, 708 16FFF2CC14E3DBF700A170EC /* RMMapQuestOpenAerialSource.m in Sources */,
  709 + DD8FD7551559E4A40044D96F /* RMUserLocation.m in Sources */,
  710 + DDA6B8BE155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m in Sources */,
659 16F3581C15864135003A3AD9 /* RMMapScrollView.m in Sources */, 711 16F3581C15864135003A3AD9 /* RMMapScrollView.m in Sources */,
660 16F98C971590CFF000FF90CE /* RMShape.m in Sources */, 712 16F98C971590CFF000FF90CE /* RMShape.m in Sources */,
661 16FBF07715936BF1004ECAD1 /* RMTileSourcesContainer.m in Sources */, 713 16FBF07715936BF1004ECAD1 /* RMTileSourcesContainer.m in Sources */,