Authored by Justin R. Miller

added user location services

... ... @@ -73,6 +73,9 @@
// This is for the QuadTree. Don't mess this up.
@property (nonatomic, assign) RMQuadTreeNode *quadTreeNode;
// This is for filtering framework-provided annotations.
@property (nonatomic, assign, readonly) BOOL isUserLocationAnnotation;
#pragma mark -
+ (id)annotationWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle;
... ...
... ... @@ -50,6 +50,7 @@
@synthesize enabled, clusteringEnabled;
@synthesize position;
@synthesize quadTreeNode;
@synthesize isUserLocationAnnotation;
+ (id)annotationWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle
{
... ... @@ -178,6 +179,11 @@
return (layer != nil);
}
- (void)setIsUserLocationAnnotation:(BOOL)flag
{
isUserLocationAnnotation = flag;
}
#pragma mark -
- (void)setBoundingBoxCoordinatesSouthWest:(CLLocationCoordinate2D)southWest northEast:(CLLocationCoordinate2D)northEast
... ...
... ... @@ -23,7 +23,7 @@
if (!(self = [super initWithFrame:frame]))
return nil;
self.layer.masksToBounds = YES;
self.layer.masksToBounds = NO;
return self;
}
... ...
... ... @@ -38,6 +38,10 @@
#import "RMMapScrollView.h"
#import "RMTileSourcesContainer.h"
#define kRMUserLocationAnnotationTypeName @"RMUserLocationAnnotation"
#define kRMTrackingHaloAnnotationTypeName @"RMTrackingHaloAnnotation"
#define kRMAccuracyCircleAnnotationTypeName @"RMAccuracyCircleAnnotation"
@class RMProjection;
@class RMFractalTileProjection;
@class RMTileCache;
... ... @@ -47,6 +51,7 @@
@class RMMarker;
@class RMAnnotation;
@class RMQuadTree;
@class RMUserLocation;
// constants for boundingMask
... ... @@ -64,7 +69,7 @@ typedef enum : NSUInteger {
} RMMapDecelerationMode;
@interface RMMapView : UIView <UIScrollViewDelegate, UIGestureRecognizerDelegate, RMMapScrollViewDelegate>
@interface RMMapView : UIView <UIScrollViewDelegate, UIGestureRecognizerDelegate, RMMapScrollViewDelegate, CLLocationManagerDelegate>
@property (nonatomic, assign) id <RMMapViewDelegate> delegate;
... ... @@ -83,6 +88,11 @@ typedef enum : NSUInteger {
@property (nonatomic, assign) BOOL adjustTilesForRetinaDisplay;
@property (nonatomic, readonly) float adjustedZoomForRetinaDisplay; // takes adjustTilesForRetinaDisplay and screen scale into account
@property (nonatomic) BOOL showsUserLocation;
@property (nonatomic, readonly, retain) RMUserLocation *userLocation;
@property (nonatomic, readonly, getter=isUserLocationVisible) BOOL userLocationVisible;
@property (nonatomic) RMUserTrackingMode userTrackingMode;
// take missing tiles from lower zoom levels, up to #missingTilesDepth zoom levels (defaults to 0, which disables this feature)
@property (nonatomic, assign) NSUInteger missingTilesDepth;
... ... @@ -235,4 +245,9 @@ typedef enum : NSUInteger {
- (RMSphericalTrapezium)latitudeLongitudeBoundingBoxForTile:(RMTile)aTile;
#pragma mark -
#pragma mark User Location
- (void)setUserTrackingMode:(RMUserTrackingMode)mode animated:(BOOL)animated;
@end
... ...
... ... @@ -33,6 +33,7 @@
#import "RMProjection.h"
#import "RMMarker.h"
#import "RMPath.h"
#import "RMCircle.h"
#import "RMShape.h"
#import "RMAnnotation.h"
#import "RMQuadTree.h"
... ... @@ -46,6 +47,8 @@
#import "RMMapTiledLayerView.h"
#import "RMMapOverlayView.h"
#import "RMUserLocation.h"
#pragma mark --- begin constants ----
#define kZoomRectPixelBuffer 150.0
... ... @@ -61,6 +64,8 @@
@interface RMMapView (PrivateMethods)
@property (nonatomic, retain) RMUserLocation *userLocation;
- (void)createMapView;
- (void)correctPositionOfAllAnnotations;
... ... @@ -72,6 +77,24 @@
#pragma mark -
@interface RMUserLocation (PrivateMethods)
@property (nonatomic, getter=isUpdating) BOOL updating;
@property (nonatomic, retain) CLLocation *location;
@property (nonatomic, retain) CLHeading *heading;
@end
#pragma mark -
@interface RMAnnotation (PrivateMethods)
@property (nonatomic, assign) BOOL isUserLocationAnnotation;
@end
#pragma mark -
@implementation RMMapView
{
id <RMMapViewDelegate> _delegate;
... ... @@ -96,6 +119,11 @@
BOOL _delegateHasLayerForAnnotation;
BOOL _delegateHasWillHideLayerForAnnotation;
BOOL _delegateHasDidHideLayerForAnnotation;
BOOL _delegateHasWillStartLocatingUser;
BOOL _delegateHasDidStopLocatingUser;
BOOL _delegateHasDidUpdateUserLocation;
BOOL _delegateHasDidFailToLocateUserWithError;
BOOL _delegateHasDidChangeUserTrackingMode;
UIView *_backgroundView;
RMMapScrollView *_mapScrollView;
... ... @@ -122,6 +150,14 @@
CGPoint _lastDraggingTranslation;
RMAnnotation *_draggedAnnotation;
CLLocationManager *locationManager;
RMUserLocation *userLocation;
BOOL showsUserLocation;
RMUserTrackingMode userTrackingMode;
UIImageView *userLocationTrackingView;
UIImageView *userHeadingTrackingView;
}
@synthesize decelerationMode = _decelerationMode;
... ... @@ -136,6 +172,7 @@
@synthesize positionClusterMarkersAtTheGravityCenter = _positionClusterMarkersAtTheGravityCenter;
@synthesize clusterMarkerSize = _clusterMarkerSize, clusterAreaSize = _clusterAreaSize;
@synthesize adjustTilesForRetinaDisplay = _adjustTilesForRetinaDisplay;
@synthesize userLocation, showsUserLocation, userTrackingMode;
@synthesize missingTilesDepth = _missingTilesDepth;
@synthesize debugTiles = _debugTiles;
... ... @@ -157,6 +194,8 @@
self.backgroundColor = [UIColor grayColor];
self.clipsToBounds = YES;
_tileSourcesContainer = [RMTileSourcesContainer new];
_tiledLayersSuperview = nil;
... ... @@ -311,6 +350,10 @@
[_projection release]; _projection = nil;
[_mercatorToTileProjection release]; _mercatorToTileProjection = nil;
[self setTileCache:nil];
[locationManager release]; locationManager = nil;
[userLocation release]; userLocation = nil;
[userLocationTrackingView release]; userLocationTrackingView = nil;
[userHeadingTrackingView release]; userHeadingTrackingView = nil;
[super dealloc];
}
... ... @@ -375,6 +418,12 @@
_delegateHasLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:layerForAnnotation:)];
_delegateHasWillHideLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:willHideLayerForAnnotation:)];
_delegateHasDidHideLayerForAnnotation = [_delegate respondsToSelector:@selector(mapView:didHideLayerForAnnotation:)];
_delegateHasWillStartLocatingUser = [_delegate respondsToSelector:@selector(mapViewWillStartLocatingUser:)];
_delegateHasDidStopLocatingUser = [_delegate respondsToSelector:@selector(mapViewDidStopLocatingUser:)];
_delegateHasDidUpdateUserLocation = [_delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)];
_delegateHasDidFailToLocateUserWithError = [_delegate respondsToSelector:@selector(mapView:didFailToLocateUserWithError:)];
_delegateHasDidChangeUserTrackingMode = [_delegate respondsToSelector:@selector(mapView:didChangeUserTrackingMode:animated:)];
}
#pragma mark -
... ... @@ -768,6 +817,9 @@
- (void)zoomInToNextNativeZoomAt:(CGPoint)pivot animated:(BOOL)animated
{
if (self.userTrackingMode != RMUserTrackingModeNone && ! CGPointEqualToPoint(pivot, self.center))
self.userTrackingMode = RMUserTrackingModeNone;
// Calculate rounded zoom
float newZoom = fmin(ceilf([self zoom]) + 0.99, [self maxZoom]);
... ... @@ -930,6 +982,7 @@
_mapScrollView.minimumZoomScale = exp2f([self minZoom]);
_mapScrollView.maximumZoomScale = exp2f([self maxZoom]);
_mapScrollView.contentOffset = CGPointMake(0.0, 0.0);
_mapScrollView.clipsToBounds = NO;
_tiledLayersSuperview = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, contentSize.width, contentSize.height)];
_tiledLayersSuperview.userInteractionEnabled = NO;
... ... @@ -1022,6 +1075,9 @@
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (self.userTrackingMode != RMUserTrackingModeNone)
self.userTrackingMode = RMUserTrackingModeNone;
if (_delegateHasBeforeMapMove)
[_delegate beforeMapMove:self];
}
... ... @@ -1067,8 +1123,14 @@
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
if (self.userTrackingMode != RMUserTrackingModeNone && scrollView.pinchGestureRecognizer.state == UIGestureRecognizerStateChanged)
self.userTrackingMode = RMUserTrackingModeNone;
[self correctPositionOfAllAnnotations];
if (_zoom < 3 && self.userTrackingMode == RMUserTrackingModeFollowWithHeading)
self.userTrackingMode = RMUserTrackingModeFollow;
if (_delegateHasAfterMapZoom)
[_delegate afterMapZoom:self];
}
... ... @@ -2200,15 +2262,18 @@
for (RMAnnotation *annotation in previousVisibleAnnotations)
{
if (_delegateHasWillHideLayerForAnnotation)
[_delegate mapView:self willHideLayerForAnnotation:annotation];
if ( ! annotation.isUserLocationAnnotation)
{
if (_delegateHasWillHideLayerForAnnotation)
[_delegate mapView:self willHideLayerForAnnotation:annotation];
annotation.layer = nil;
annotation.layer = nil;
if (_delegateHasDidHideLayerForAnnotation)
[_delegate mapView:self didHideLayerForAnnotation:annotation];
if (_delegateHasDidHideLayerForAnnotation)
[_delegate mapView:self didHideLayerForAnnotation:annotation];
[_visibleAnnotations removeObject:annotation];
[_visibleAnnotations removeObject:annotation];
}
}
[previousVisibleAnnotations release];
... ... @@ -2248,14 +2313,17 @@
}
else
{
if (_delegateHasWillHideLayerForAnnotation)
[_delegate mapView:self willHideLayerForAnnotation:annotation];
if ( ! annotation.isUserLocationAnnotation)
{
if (_delegateHasWillHideLayerForAnnotation)
[_delegate mapView:self willHideLayerForAnnotation:annotation];
annotation.layer = nil;
[_visibleAnnotations removeObject:annotation];
annotation.layer = nil;
[_visibleAnnotations removeObject:annotation];
if (_delegateHasDidHideLayerForAnnotation)
[_delegate mapView:self didHideLayerForAnnotation:annotation];
if (_delegateHasDidHideLayerForAnnotation)
[_delegate mapView:self didHideLayerForAnnotation:annotation];
}
}
}
// RMLog(@"%d annotations on screen, %d total", [overlayView sublayersCount], [annotations count]);
... ... @@ -2346,10 +2414,13 @@
{
for (RMAnnotation *annotation in annotationsToRemove)
{
[_annotations removeObject:annotation];
[_visibleAnnotations removeObject:annotation];
[self.quadTree removeAnnotation:annotation];
annotation.layer = nil;
if ( ! annotation.isUserLocationAnnotation)
{
[_annotations removeObject:annotation];
[_visibleAnnotations removeObject:annotation];
[self.quadTree removeAnnotation:annotation];
annotation.layer = nil;
}
}
}
... ... @@ -2358,25 +2429,401 @@
- (void)removeAllAnnotations
{
@synchronized (_annotations)
[self removeAnnotations:[_annotations allObjects]];
}
- (CGPoint)mapPositionForAnnotation:(RMAnnotation *)annotation
{
[self correctScreenPosition:annotation animated:NO];
return annotation.position;
}
#pragma mark -
#pragma mark User Location
- (void)setShowsUserLocation:(BOOL)newShowsUserLocation
{
if (newShowsUserLocation == showsUserLocation)
return;
showsUserLocation = newShowsUserLocation;
if (newShowsUserLocation)
{
if (_delegateHasWillStartLocatingUser)
[_delegate mapViewWillStartLocatingUser:self];
self.userLocation = [RMUserLocation annotationWithMapView:self coordinate:CLLocationCoordinate2DMake(0, 0) andTitle:nil];
locationManager = [[CLLocationManager alloc] init];
locationManager.headingFilter = 5;
locationManager.delegate = self;
[locationManager startUpdatingLocation];
}
else
{
[locationManager stopUpdatingLocation];
[locationManager stopUpdatingHeading];
locationManager.delegate = nil;
[locationManager release];
locationManager = nil;
if (_delegateHasDidStopLocatingUser)
[_delegate mapViewDidStopLocatingUser:self];
[self setUserTrackingMode:RMUserTrackingModeNone animated:YES];
NSMutableArray *annotationsToRemove = [NSMutableArray array];
for (RMAnnotation *annotation in _annotations)
if (annotation.isUserLocationAnnotation)
[annotationsToRemove addObject:annotation];
for (RMAnnotation *annotationToRemove in annotationsToRemove)
[self removeAnnotation:annotationToRemove];
self.userLocation = nil;
}
}
- (void)setUserLocation:(RMUserLocation *)newUserLocation
{
if ( ! [newUserLocation isEqual:userLocation])
{
[userLocation release];
userLocation = [newUserLocation retain];
}
}
- (BOOL)isUserLocationVisible
{
if (userLocation)
{
CGPoint locationPoint = [self mapPositionForAnnotation:userLocation];
CGRect locationRect = CGRectMake(locationPoint.x - userLocation.location.horizontalAccuracy,
locationPoint.y - userLocation.location.horizontalAccuracy,
userLocation.location.horizontalAccuracy * 2,
userLocation.location.horizontalAccuracy * 2);
return CGRectIntersectsRect([self bounds], locationRect);
}
return NO;
}
- (void)setUserTrackingMode:(RMUserTrackingMode)mode
{
[self setUserTrackingMode:mode animated:YES];
}
- (void)setUserTrackingMode:(RMUserTrackingMode)mode animated:(BOOL)animated
{
if (mode == userTrackingMode)
return;
userTrackingMode = mode;
switch (userTrackingMode)
{
case RMUserTrackingModeNone:
default:
{
[locationManager stopUpdatingHeading];
[UIView animateWithDuration:(animated ? 0.5 : 0.0)
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
animations:^(void)
{
_mapScrollView.transform = CGAffineTransformIdentity;
_overlayView.transform = CGAffineTransformIdentity;
for (RMAnnotation *annotation in _annotations)
if ( ! annotation.isUserLocationAnnotation)
annotation.layer.transform = CATransform3DIdentity;
}
completion:nil];
if (userLocationTrackingView || userHeadingTrackingView)
{
[userLocationTrackingView removeFromSuperview];
userLocationTrackingView = nil;
[userHeadingTrackingView removeFromSuperview];
userHeadingTrackingView = nil;
}
userLocation.layer.hidden = NO;
break;
}
case RMUserTrackingModeFollow:
{
self.showsUserLocation = YES;
[locationManager stopUpdatingHeading];
if (self.userLocation)
[self locationManager:locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location];
if (userLocationTrackingView || userHeadingTrackingView)
{
[userLocationTrackingView removeFromSuperview];
userLocationTrackingView = nil;
[userHeadingTrackingView removeFromSuperview];
userHeadingTrackingView = nil;
}
[UIView animateWithDuration:(animated ? 0.5 : 0.0)
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
animations:^(void)
{
_mapScrollView.transform = CGAffineTransformIdentity;
_overlayView.transform = CGAffineTransformIdentity;
for (RMAnnotation *annotation in _annotations)
if ( ! annotation.isUserLocationAnnotation)
annotation.layer.transform = CATransform3DIdentity;
}
completion:nil];
userLocation.layer.hidden = NO;
break;
}
case RMUserTrackingModeFollowWithHeading:
{
// Remove the layer from the screen
annotation.layer = nil;
self.showsUserLocation = YES;
userLocation.layer.hidden = YES;
userHeadingTrackingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"HeadingAngleSmall.png"]];
userHeadingTrackingView.center = CGPointMake(round([self bounds].size.width / 2),
round([self bounds].size.height / 2) - (userHeadingTrackingView.bounds.size.height / 2) - 4);
userHeadingTrackingView.alpha = 0.0;
[self addSubview:userHeadingTrackingView];
[UIView animateWithDuration:0.5 animations:^(void) { userHeadingTrackingView.alpha = 1.0; }];
userLocationTrackingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"TrackingDot.png"]];
userLocationTrackingView.center = CGPointMake(round([self bounds].size.width / 2),
round([self bounds].size.height / 2));
[self addSubview:userLocationTrackingView];
if (self.zoom < 3)
[self zoomByFactor:exp2f(3 - [self zoom]) near:self.center animated:YES];
if (self.userLocation)
[self locationManager:locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location];
[locationManager startUpdatingHeading];
break;
}
}
[_annotations removeAllObjects];
[_visibleAnnotations removeAllObjects];
[self.quadTree removeAllObjects];
[self correctPositionOfAllAnnotations];
if (_delegateHasDidChangeUserTrackingMode)
[_delegate mapView:self didChangeUserTrackingMode:userTrackingMode animated:animated];
}
- (CGPoint)mapPositionForAnnotation:(RMAnnotation *)annotation
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
[self correctScreenPosition:annotation animated:NO];
return annotation.position;
if ( ! showsUserLocation || _mapScrollView.isDragging)
return;
if ([newLocation distanceFromLocation:oldLocation])
{
userLocation.location = newLocation;
if (_delegateHasDidUpdateUserLocation)
[_delegate mapView:self didUpdateUserLocation:userLocation];
}
if (self.userTrackingMode != RMUserTrackingModeNone)
{
// zoom centered on user location unless we're already centered there (or very close)
//
CGPoint mapCenterPoint = [self convertPoint:self.center fromView:self.superview];
CGPoint userLocationPoint = [self mapPositionForAnnotation:userLocation];
if (fabsf(userLocationPoint.x - mapCenterPoint.x) > 2 || fabsf(userLocationPoint.y - mapCenterPoint.y > 2))
{
float delta = newLocation.horizontalAccuracy / 110000; // approx. meter per degree latitude
CLLocationCoordinate2D southWest = CLLocationCoordinate2DMake(newLocation.coordinate.latitude - delta,
newLocation.coordinate.longitude - delta);
CLLocationCoordinate2D northEast = CLLocationCoordinate2DMake(newLocation.coordinate.latitude + delta,
newLocation.coordinate.longitude + delta);
if (northEast.latitude != [self latitudeLongitudeBoundingBox].northEast.latitude ||
northEast.longitude != [self latitudeLongitudeBoundingBox].northEast.longitude ||
southWest.latitude != [self latitudeLongitudeBoundingBox].southWest.latitude ||
southWest.longitude != [self latitudeLongitudeBoundingBox].southWest.longitude)
[self zoomWithLatitudeLongitudeBoundsSouthWest:southWest northEast:northEast animated:YES];
}
}
RMAnnotation *accuracyCircleAnnotation = nil;
for (RMAnnotation *annotation in _annotations)
if ([annotation.annotationType isEqualToString:kRMAccuracyCircleAnnotationTypeName])
accuracyCircleAnnotation = annotation;
if ( ! accuracyCircleAnnotation)
{
accuracyCircleAnnotation = [RMAnnotation annotationWithMapView:self coordinate:newLocation.coordinate andTitle:nil];
accuracyCircleAnnotation.annotationType = kRMAccuracyCircleAnnotationTypeName;
accuracyCircleAnnotation.clusteringEnabled = NO;
accuracyCircleAnnotation.layer = [[RMCircle alloc] initWithView:self radiusInMeters:newLocation.horizontalAccuracy];
accuracyCircleAnnotation.isUserLocationAnnotation = YES;
((RMCircle *)accuracyCircleAnnotation.layer).lineColor = [UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.7];
((RMCircle *)accuracyCircleAnnotation.layer).fillColor = [UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.15];
((RMCircle *)accuracyCircleAnnotation.layer).lineWidthInPixels = 2.0;
[self addAnnotation:accuracyCircleAnnotation];
}
if ([newLocation distanceFromLocation:oldLocation])
accuracyCircleAnnotation.coordinate = newLocation.coordinate;
if (newLocation.horizontalAccuracy != oldLocation.horizontalAccuracy)
((RMCircle *)accuracyCircleAnnotation.layer).radiusInMeters = newLocation.horizontalAccuracy;
RMAnnotation *trackingHaloAnnotation = nil;
for (RMAnnotation *annotation in _annotations)
if ([annotation.annotationType isEqualToString:kRMTrackingHaloAnnotationTypeName])
trackingHaloAnnotation = annotation;
if ( ! trackingHaloAnnotation)
{
trackingHaloAnnotation = [RMAnnotation annotationWithMapView:self coordinate:newLocation.coordinate andTitle:nil];
trackingHaloAnnotation.annotationType = kRMTrackingHaloAnnotationTypeName;
trackingHaloAnnotation.clusteringEnabled = NO;
// create image marker
//
trackingHaloAnnotation.layer = [[RMMarker alloc] initWithUIImage:[UIImage imageNamed:@"TrackingDotHalo.png"]];
trackingHaloAnnotation.isUserLocationAnnotation = YES;
[CATransaction begin];
[CATransaction setAnimationDuration:2.5];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// scale out radially
//
CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
boundsAnimation.repeatCount = MAXFLOAT;
boundsAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
boundsAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)];
boundsAnimation.removedOnCompletion = NO;
boundsAnimation.fillMode = kCAFillModeForwards;
[trackingHaloAnnotation.layer addAnimation:boundsAnimation forKey:@"animateScale"];
// go transparent as scaled out
//
CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
opacityAnimation.repeatCount = MAXFLOAT;
opacityAnimation.fromValue = [NSNumber numberWithFloat:1.0];
opacityAnimation.toValue = [NSNumber numberWithFloat:-1.0];
opacityAnimation.removedOnCompletion = NO;
opacityAnimation.fillMode = kCAFillModeForwards;
[trackingHaloAnnotation.layer addAnimation:opacityAnimation forKey:@"animateOpacity"];
[CATransaction commit];
[self addAnnotation:trackingHaloAnnotation];
}
if ([newLocation distanceFromLocation:oldLocation])
trackingHaloAnnotation.coordinate = newLocation.coordinate;
userLocation.layer.hidden = ((trackingHaloAnnotation.coordinate.latitude == 0 && trackingHaloAnnotation.coordinate.longitude == 0) || self.userTrackingMode == RMUserTrackingModeFollowWithHeading);
accuracyCircleAnnotation.layer.hidden = newLocation.horizontalAccuracy <= 10;
trackingHaloAnnotation.layer.hidden = ((trackingHaloAnnotation.coordinate.latitude == 0 && trackingHaloAnnotation.coordinate.longitude == 0) || newLocation.horizontalAccuracy > 10);
if ( ! [_annotations containsObject:userLocation])
[self addAnnotation:userLocation];
}
- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager
{
return YES;
}
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
{
if ( ! showsUserLocation || _mapScrollView.isDragging)
return;
userLocation.heading = newHeading;
if (_delegateHasDidUpdateUserLocation)
[_delegate mapView:self didUpdateUserLocation:userLocation];
if (newHeading.trueHeading != 0 && self.userTrackingMode == RMUserTrackingModeFollowWithHeading)
{
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
animations:^(void)
{
CGFloat angle = (M_PI / -180) * newHeading.trueHeading;
_mapScrollView.transform = CGAffineTransformMakeRotation(angle);
_overlayView.transform = CGAffineTransformMakeRotation(angle);
for (RMAnnotation *annotation in _annotations)
if ( ! annotation.isUserLocationAnnotation)
annotation.layer.transform = CATransform3DMakeAffineTransform(CGAffineTransformMakeRotation(-angle));
}
completion:nil];
[CATransaction commit];
}
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
if ([error code] != kCLErrorLocationUnknown)
{
self.userTrackingMode = RMUserTrackingModeNone;
if (_delegateHasDidFailToLocateUserWithError)
[_delegate mapView:self didFailToLocateUserWithError:error];
}
}
@end
... ...
... ... @@ -29,6 +29,13 @@
@class RMMapLayer;
@class RMMarker;
@class RMAnnotation;
@class RMUserLocation;
typedef enum {
RMUserTrackingModeNone = 0,
RMUserTrackingModeFollow = 1,
RMUserTrackingModeFollowWithHeading = 2
} RMUserTrackingMode;
// Use this for notifications of map panning, zooming, and taps on the RMMapView.
@protocol RMMapViewDelegate <NSObject>
... ... @@ -67,4 +74,10 @@
- (void)mapView:(RMMapView *)map didDragAnnotation:(RMAnnotation *)annotation withDelta:(CGPoint)delta;
- (void)mapView:(RMMapView *)map didEndDragAnnotation:(RMAnnotation *)annotation;
- (void)mapViewWillStartLocatingUser:(RMMapView *)mapView;
- (void)mapViewDidStopLocatingUser:(RMMapView *)mapView;
- (void)mapView:(RMMapView *)mapView didUpdateUserLocation:(RMUserLocation *)userLocation;
- (void)mapView:(RMMapView *)mapView didFailToLocateUserWithError:(NSError *)error;
- (void)mapView:(RMMapView *)mapView didChangeUserTrackingMode:(RMUserTrackingMode)mode animated:(BOOL)animated;
@end
... ...
//
// RMUserLocation.h
// MapView
//
// Created by Justin Miller on 5/8/12.
// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
//
#import "RMAnnotation.h"
@interface RMUserLocation : RMAnnotation
@property (nonatomic, readonly, getter=isUpdating) BOOL updating;
@property (nonatomic, readonly, retain) CLLocation *location;
@property (nonatomic, readonly, retain) CLHeading *heading;
@end
... ...
//
// RMUserLocation.m
// MapView
//
// Created by Justin Miller on 5/8/12.
// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
//
#import "RMUserLocation.h"
#import "RMMarker.h"
#import "RMMapView.h"
@implementation RMUserLocation
@synthesize updating;
@synthesize location;
@synthesize heading;
- (id)initWithMapView:(RMMapView *)aMapView coordinate:(CLLocationCoordinate2D)aCoordinate andTitle:(NSString *)aTitle
{
if ( ! (self = [super initWithMapView:aMapView coordinate:aCoordinate andTitle:aTitle]))
return nil;
NSAssert([[NSBundle mainBundle] pathForResource:@"TrackingDot" ofType:@"png"], @"Unable to find necessary user location graphical assets (copy from MapView/Map/Resources)");
layer = [[RMMarker alloc] initWithUIImage:[UIImage imageNamed:@"TrackingDot.png"]];
annotationType = [kRMUserLocationAnnotationTypeName retain];
clusteringEnabled = NO;
return self;
}
- (void)dealloc
{
[layer release]; layer = nil;
[annotationType release]; annotationType = nil;
[location release]; location = nil;
[heading release]; heading = nil;
[super dealloc];
}
- (BOOL)isUpdating
{
return (self.mapView.userTrackingMode != RMUserTrackingModeNone);
}
- (void)setLocation:(CLLocation *)newLocation
{
if ([newLocation distanceFromLocation:location] && newLocation.coordinate.latitude != 0 && newLocation.coordinate.longitude != 0)
{
[self willChangeValueForKey:@"location"];
[location release];
location = [newLocation retain];
self.coordinate = location.coordinate;
[self didChangeValueForKey:@"location"];
}
}
- (void)setHeading:(CLHeading *)newHeading
{
if (newHeading.trueHeading != heading.trueHeading)
{
[self willChangeValueForKey:@"heading"];
[heading release];
heading = [newHeading retain];
[self didChangeValueForKey:@"heading"];
}
}
- (BOOL)isUserLocationAnnotation
{
return YES;
}
@end
... ...
//
// RMUserTrackingBarButtonItem.h
// MapView
//
// Created by Justin Miller on 5/10/12.
// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
//
#import <UIKit/UIKit.h>
@class RMMapView;
@interface RMUserTrackingBarButtonItem : UIBarButtonItem
- (id)initWithMapView:(RMMapView *)mapView;
@property (nonatomic, retain) RMMapView *mapView;
@end
... ...
//
// RMUserTrackingBarButtonItem.m
// MapView
//
// Created by Justin Miller on 5/10/12.
// Copyright (c) 2012 MapBox / Development Seed. All rights reserved.
//
#import "RMUserTrackingBarButtonItem.h"
#import "RMMapView.h"
#import "RMUserLocation.h"
typedef enum {
RMUserTrackingButtonStateActivity = 0,
RMUserTrackingButtonStateLocation = 1,
RMUserTrackingButtonStateHeading = 2
} RMUserTrackingButtonState;
@interface RMUserTrackingBarButtonItem ()
@property (nonatomic, retain) UISegmentedControl *segmentedControl;
@property (nonatomic, retain) UIImageView *buttonImageView;
@property (nonatomic, retain) UIActivityIndicatorView *activityView;
@property (nonatomic) RMUserTrackingButtonState state;
- (void)updateAppearance;
- (void)changeMode:(id)sender;
@end
#pragma mark -
@implementation RMUserTrackingBarButtonItem
@synthesize mapView=_mapView;
@synthesize segmentedControl;
@synthesize buttonImageView;
@synthesize activityView;
@synthesize state;
- (id)initWithMapView:(RMMapView *)mapView
{
if (!(self = [super initWithCustomView:[[UIControl alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]]))
return nil;
segmentedControl = [[[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObject:@""]] retain];
segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
[segmentedControl setWidth:32.0 forSegmentAtIndex:0];
segmentedControl.userInteractionEnabled = NO;
segmentedControl.tintColor = self.tintColor;
segmentedControl.center = self.customView.center;
[self.customView addSubview:segmentedControl];
buttonImageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"TrackingLocation.png"]] retain];
buttonImageView.contentMode = UIViewContentModeCenter;
buttonImageView.frame = CGRectMake(0, 0, 32, 32);
buttonImageView.center = self.customView.center;
buttonImageView.userInteractionEnabled = NO;
[self.customView addSubview:buttonImageView];
activityView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite] retain];
activityView.hidesWhenStopped = YES;
activityView.center = self.customView.center;
activityView.userInteractionEnabled = NO;
[self.customView addSubview:activityView];
[((UIControl *)self.customView) addTarget:self action:@selector(changeMode:) forControlEvents:UIControlEventTouchUpInside];
_mapView = [mapView retain];
[_mapView addObserver:self forKeyPath:@"userTrackingMode" options:NSKeyValueObservingOptionNew context:nil];
[_mapView addObserver:self forKeyPath:@"userLocation.location" options:NSKeyValueObservingOptionNew context:nil];
state = RMUserTrackingButtonStateLocation;
[self updateAppearance];
return self;
}
- (void)dealloc
{
[segmentedControl release]; segmentedControl = nil;
[buttonImageView release]; buttonImageView = nil;
[activityView release]; activityView = nil;
[_mapView removeObserver:self forKeyPath:@"userTrackingMode"];
[_mapView removeObserver:self forKeyPath:@"userLocation.location"];
[_mapView release]; _mapView = nil;
[super dealloc];
}
#pragma mark -
- (void)setMapView:(RMMapView *)newMapView
{
if ( ! [newMapView isEqual:_mapView])
{
[_mapView removeObserver:self forKeyPath:@"userTrackingMode"];
[_mapView removeObserver:self forKeyPath:@"userLocation.location"];
[_mapView release];
_mapView = [newMapView retain];
[_mapView addObserver:self forKeyPath:@"userTrackingMode" options:NSKeyValueObservingOptionNew context:nil];
[_mapView addObserver:self forKeyPath:@"userLocation.location" options:NSKeyValueObservingOptionNew context:nil];
[self updateAppearance];
}
}
- (void)setTintColor:(UIColor *)newTintColor
{
[super setTintColor:newTintColor];
segmentedControl.tintColor = newTintColor;
}
#pragma mark -
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
[self updateAppearance];
}
#pragma mark -
- (void)updateAppearance
{
// "selection" state
//
segmentedControl.selectedSegmentIndex = (_mapView.userTrackingMode == RMUserTrackingModeNone ? UISegmentedControlNoSegment : 0);
// activity/image state
//
if (_mapView.userTrackingMode != RMUserTrackingModeNone && ( ! _mapView.userLocation || ! _mapView.userLocation.location || (_mapView.userLocation.location.coordinate.latitude == 0 && _mapView.userLocation.location.coordinate.longitude == 0)))
{
// if we should be tracking but don't yet have a location, show activity
//
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^(void)
{
buttonImageView.transform = CGAffineTransformMakeScale(0.01, 0.01);
activityView.transform = CGAffineTransformMakeScale(0.01, 0.01);
}
completion:^(BOOL finished)
{
buttonImageView.hidden = YES;
[activityView startAnimating];
[UIView animateWithDuration:0.25 animations:^(void)
{
buttonImageView.transform = CGAffineTransformIdentity;
activityView.transform = CGAffineTransformIdentity;
}];
}];
state = RMUserTrackingButtonStateActivity;
}
else
{
if ((_mapView.userTrackingMode != RMUserTrackingModeFollowWithHeading && state != RMUserTrackingButtonStateLocation) ||
(_mapView.userTrackingMode == RMUserTrackingModeFollowWithHeading && state != RMUserTrackingButtonStateHeading))
{
// if image state doesn't match mode, update it
//
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^(void)
{
buttonImageView.transform = CGAffineTransformMakeScale(0.01, 0.01);
activityView.transform = CGAffineTransformMakeScale(0.01, 0.01);
}
completion:^(BOOL finished)
{
buttonImageView.image = [UIImage imageNamed:(_mapView.userTrackingMode == RMUserTrackingModeFollowWithHeading ? @"TrackingHeading.png" : @"TrackingLocation.png")];
buttonImageView.hidden = NO;
[activityView stopAnimating];
[UIView animateWithDuration:0.25 animations:^(void)
{
buttonImageView.transform = CGAffineTransformIdentity;
activityView.transform = CGAffineTransformIdentity;
}];
}];
state = (_mapView.userTrackingMode == RMUserTrackingModeFollowWithHeading ? RMUserTrackingButtonStateHeading : RMUserTrackingButtonStateLocation);
}
}
}
- (void)changeMode:(id)sender
{
if (_mapView)
{
switch (_mapView.userTrackingMode)
{
case RMUserTrackingModeNone:
default:
{
_mapView.userTrackingMode = RMUserTrackingModeFollow;
break;
}
case RMUserTrackingModeFollow:
{
if ([CLLocationManager headingAvailable])
_mapView.userTrackingMode = RMUserTrackingModeFollowWithHeading;
else
_mapView.userTrackingMode = RMUserTrackingModeNone;
break;
}
case RMUserTrackingModeFollowWithHeading:
{
_mapView.userTrackingMode = RMUserTrackingModeNone;
break;
}
}
}
[self updateAppearance];
}
@end
... ...
... ... @@ -69,3 +69,5 @@
#import "RMTileImage.h"
#import "RMTileSource.h"
#import "RMTileSourcesContainer.h"
#import "RMUserLocation.h"
#import "RMUserTrackingBarButtonItem.h"
... ...
... ... @@ -101,6 +101,10 @@
DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */; };
DD98B6FA14D76B930092882F /* RMMapBoxSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DD98B6F814D76B930092882F /* RMMapBoxSource.h */; };
DD98B6FB14D76B930092882F /* RMMapBoxSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD98B6F914D76B930092882F /* RMMapBoxSource.m */; };
DD8FD7541559E4A40044D96F /* RMUserLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8FD7521559E4A40044D96F /* RMUserLocation.h */; };
DD8FD7551559E4A40044D96F /* RMUserLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8FD7531559E4A40044D96F /* RMUserLocation.m */; };
DDA6B8BD155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */; };
DDA6B8BE155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
... ... @@ -230,6 +234,20 @@
DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapQuestOSMSource.m; sourceTree = "<group>"; };
DD98B6F814D76B930092882F /* RMMapBoxSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapBoxSource.h; sourceTree = "<group>"; };
DD98B6F914D76B930092882F /* RMMapBoxSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapBoxSource.m; sourceTree = "<group>"; };
DD8FD7521559E4A40044D96F /* RMUserLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMUserLocation.h; sourceTree = "<group>"; };
DD8FD7531559E4A40044D96F /* RMUserLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMUserLocation.m; sourceTree = "<group>"; };
DD8FD7631559EE120044D96F /* HeadingAngleSmall.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = HeadingAngleSmall.png; path = Resources/HeadingAngleSmall.png; sourceTree = "<group>"; };
DD8FD7641559EE120044D96F /* HeadingAngleSmall@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "HeadingAngleSmall@2x.png"; path = "Resources/HeadingAngleSmall@2x.png"; sourceTree = "<group>"; };
DD8FD7651559EE120044D96F /* TrackingDot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingDot.png; path = Resources/TrackingDot.png; sourceTree = "<group>"; };
DD8FD7661559EE120044D96F /* TrackingDot@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingDot@2x.png"; path = "Resources/TrackingDot@2x.png"; sourceTree = "<group>"; };
DD8FD7691559EE120044D96F /* TrackingDotHalo@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingDotHalo@2x.png"; path = "Resources/TrackingDotHalo@2x.png"; sourceTree = "<group>"; };
DD8FD76C1559EE120044D96F /* TrackingDotHalo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingDotHalo.png; path = Resources/TrackingDotHalo.png; sourceTree = "<group>"; };
DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMUserTrackingBarButtonItem.h; sourceTree = "<group>"; };
DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMUserTrackingBarButtonItem.m; sourceTree = "<group>"; };
DDA6B8C0155CAB9A003DB5D8 /* TrackingLocation.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingLocation.png; path = Resources/TrackingLocation.png; sourceTree = "<group>"; };
DDA6B8C1155CAB9A003DB5D8 /* TrackingLocation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingLocation@2x.png"; path = "Resources/TrackingLocation@2x.png"; sourceTree = "<group>"; };
DDA6B8C2155CAB9A003DB5D8 /* TrackingHeading.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = TrackingHeading.png; path = Resources/TrackingHeading.png; sourceTree = "<group>"; };
DDA6B8C3155CAB9A003DB5D8 /* TrackingHeading@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "TrackingHeading@2x.png"; path = "Resources/TrackingHeading@2x.png"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
... ... @@ -476,6 +494,7 @@
B83E64EB0E80E73F001663B6 /* Tile Source */,
B83E64CE0E80E73F001663B6 /* Tile Layer & Overlay */,
B86F26A80E8742ED007A3773 /* Markers and other layers */,
DD8FD7571559ED930044D96F /* User Location */,
1266929E0EB75BEA00E002D5 /* Configuration */,
B8474B8C0EB40094006A0BC1 /* FMDB */,
);
... ... @@ -495,6 +514,35 @@
name = "Coordinate Systems";
sourceTree = "<group>";
};
DD8FD7571559ED930044D96F /* User Location */ = {
isa = PBXGroup;
children = (
DD8FD7521559E4A40044D96F /* RMUserLocation.h */,
DD8FD7531559E4A40044D96F /* RMUserLocation.m */,
DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */,
DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */,
DD8FD7581559EDA80044D96F /* Resources */,
);
name = "User Location";
sourceTree = "<group>";
};
DD8FD7581559EDA80044D96F /* Resources */ = {
isa = PBXGroup;
children = (
DD8FD7631559EE120044D96F /* HeadingAngleSmall.png */,
DD8FD7641559EE120044D96F /* HeadingAngleSmall@2x.png */,
DD8FD7651559EE120044D96F /* TrackingDot.png */,
DD8FD7661559EE120044D96F /* TrackingDot@2x.png */,
DD8FD76C1559EE120044D96F /* TrackingDotHalo.png */,
DD8FD7691559EE120044D96F /* TrackingDotHalo@2x.png */,
DDA6B8C2155CAB9A003DB5D8 /* TrackingHeading.png */,
DDA6B8C3155CAB9A003DB5D8 /* TrackingHeading@2x.png */,
DDA6B8C0155CAB9A003DB5D8 /* TrackingLocation.png */,
DDA6B8C1155CAB9A003DB5D8 /* TrackingLocation@2x.png */,
);
name = Resources;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
... ... @@ -543,6 +591,8 @@
DD8CDB4A14E0507100B73EB9 /* RMMapQuestOSMSource.h in Headers */,
1607499514E120A100D535F5 /* RMGenericMapSource.h in Headers */,
16FFF2CB14E3DBF700A170EC /* RMMapQuestOpenAerialSource.h in Headers */,
DD8FD7541559E4A40044D96F /* RMUserLocation.h in Headers */,
DDA6B8BD155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h in Headers */,
16F3581B15864135003A3AD9 /* RMMapScrollView.h in Headers */,
16F98C961590CFF000FF90CE /* RMShape.h in Headers */,
16FBF07615936BF1004ECAD1 /* RMTileSourcesContainer.h in Headers */,
... ... @@ -656,6 +706,8 @@
DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */,
1607499614E120A100D535F5 /* RMGenericMapSource.m in Sources */,
16FFF2CC14E3DBF700A170EC /* RMMapQuestOpenAerialSource.m in Sources */,
DD8FD7551559E4A40044D96F /* RMUserLocation.m in Sources */,
DDA6B8BE155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m in Sources */,
16F3581C15864135003A3AD9 /* RMMapScrollView.m in Sources */,
16F98C971590CFF000FF90CE /* RMShape.m in Sources */,
16FBF07715936BF1004ECAD1 /* RMTileSourcesContainer.m in Sources */,
... ...