Authored by Justin R. Miller

Merge branch 'location' into develop

... ... @@ -49,6 +49,10 @@ typedef enum {
RMMapDecelerationOff
} RMMapDecelerationMode;
#define kRMUserLocationAnnotationTypeName @"RMUserLocationAnnotation"
#define kRMTrackingHaloAnnotationTypeName @"RMTrackingHaloAnnotation"
#define kRMAccuracyCircleAnnotationTypeName @"RMAccuracyCircleAnnotation"
@class RMProjection;
@class RMFractalTileProjection;
@class RMTileCache;
... ... @@ -57,12 +61,13 @@ typedef enum {
@class RMMarker;
@class RMAnnotation;
@class RMQuadTree;
@class RMUserLocation;
@protocol RMMercatorToTileProjection;
@protocol RMTileSource;
@protocol RMMapTiledLayerViewDelegate;
@interface RMMapView : UIView <UIScrollViewDelegate, RMMapOverlayViewDelegate, RMMapTiledLayerViewDelegate>
@interface RMMapView : UIView <UIScrollViewDelegate, RMMapOverlayViewDelegate, RMMapTiledLayerViewDelegate, CLLocationManagerDelegate>
{
id <RMMapViewDelegate> delegate;
... ... @@ -94,6 +99,14 @@ typedef enum {
float screenScale;
NSUInteger boundingMask;
CLLocationManager *locationManager;
RMUserLocation *userLocation;
BOOL showsUserLocation;
RMUserTrackingMode userTrackingMode;
UIImageView *userLocationTrackingView;
UIImageView *userHeadingTrackingView;
}
@property (nonatomic, assign) id <RMMapViewDelegate> delegate;
... ... @@ -136,6 +149,11 @@ typedef enum {
@property (nonatomic, retain) UIView *backgroundView;
@property (nonatomic) BOOL showsUserLocation;
@property (nonatomic, readonly, retain) RMUserLocation *userLocation;
@property (nonatomic, readonly, getter=isUserLocationVisible) BOOL userLocationVisible;
@property (nonatomic) RMUserTrackingMode userTrackingMode;
#pragma mark -
#pragma mark Initializers
... ... @@ -239,4 +257,9 @@ typedef enum {
- (UIImage *)takeSnapshot;
- (UIImage *)takeSnapshotAndIncludeOverlay:(BOOL)includeOverlay;
#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 "RMAnnotation.h"
#import "RMQuadTree.h"
... ... @@ -45,6 +46,8 @@
#import "RMMapTiledLayerView.h"
#import "RMMapOverlayView.h"
#import "RMUserLocation.h"
#pragma mark --- begin constants ----
#define kiPhoneMilimeteresPerPixel .1543
... ... @@ -62,6 +65,7 @@
@interface RMMapView (PrivateMethods)
@property (nonatomic, retain) RMMapLayer *overlay;
@property (nonatomic, retain) RMUserLocation *userLocation;
- (void)createMapView;
... ... @@ -74,6 +78,16 @@
#pragma mark -
@interface RMUserLocation (PrivateMethods)
@property (nonatomic, getter=isUpdating) BOOL updating;
@property (nonatomic, retain) CLLocation *location;
@property (nonatomic, retain) CLHeading *heading;
@end
#pragma mark -
@implementation RMMapView
{
BOOL _delegateHasBeforeMapMove;
... ... @@ -95,6 +109,11 @@
BOOL _delegateHasLayerForAnnotation;
BOOL _delegateHasWillHideLayerForAnnotation;
BOOL _delegateHasDidHideLayerForAnnotation;
BOOL _delegateHasWillStartLocatingUser;
BOOL _delegateHasDidStopLocatingUser;
BOOL _delegateHasDidUpdateUserLocation;
BOOL _delegateHasDidFailToLocateUserWithError;
BOOL _delegateHasDidChangeUserTrackingMode;
BOOL _constrainMovement;
RMProjectedRect _constrainingProjectedBounds;
... ... @@ -113,6 +132,7 @@
@synthesize quadTree;
@synthesize enableClustering, positionClusterMarkersAtTheGravityCenter, clusterMarkerSize, clusterAreaSize;
@synthesize adjustTilesForRetinaDisplay;
@synthesize userLocation, showsUserLocation, userTrackingMode;
#pragma mark -
#pragma mark Initialization
... ... @@ -128,6 +148,8 @@
self.backgroundColor = [UIColor grayColor];
self.clipsToBounds = YES;
tileSource = nil;
projection = nil;
mercatorToTileProjection = nil;
... ... @@ -278,6 +300,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];
}
... ... @@ -338,6 +364,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:)];
}
- (id <RMMapViewDelegate>)delegate
... ... @@ -721,6 +753,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]);
... ... @@ -942,6 +977,7 @@
mapScrollView.minimumZoomScale = exp2f([self minZoom]);
mapScrollView.maximumZoomScale = exp2f([self maxZoom]);
mapScrollView.contentOffset = CGPointMake(0.0, 0.0);
mapScrollView.clipsToBounds = NO;
tiledLayerView = [[RMMapTiledLayerView alloc] initWithFrame:CGRectMake(0.0, 0.0, contentSize.width, contentSize.height) mapView:self];
tiledLayerView.delegate = self;
... ... @@ -988,6 +1024,9 @@
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (self.userTrackingMode != RMUserTrackingModeNone)
self.userTrackingMode = RMUserTrackingModeNone;
if (_delegateHasBeforeMapMove)
[delegate beforeMapMove:self];
}
... ... @@ -1033,8 +1072,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];
}
... ... @@ -1129,7 +1174,14 @@
- (void)mapTiledLayerView:(RMMapTiledLayerView *)aTiledLayerView doubleTapAtPoint:(CGPoint)aPoint
{
[self zoomInToNextNativeZoomAt:aPoint animated:YES];
if (self.userTrackingMode != RMUserTrackingModeNone && CGRectContainsPoint(CGRectMake(self.center.x - 75, self.center.y - 75, 150, 150), aPoint))
{
[self zoomInToNextNativeZoomAt:self.center animated:YES];
}
else
{
[self zoomInToNextNativeZoomAt:aPoint animated:YES];
}
if (_delegateHasDoubleTapOnMap)
[delegate doubleTapOnMap:self at:aPoint];
... ... @@ -1665,15 +1717,18 @@
for (RMAnnotation *annotation in previousVisibleAnnotations)
{
if (_delegateHasWillHideLayerForAnnotation)
[delegate mapView:self willHideLayerForAnnotation:annotation];
if ( ! [[NSArray arrayWithObjects:kRMUserLocationAnnotationTypeName, kRMAccuracyCircleAnnotationTypeName, kRMTrackingHaloAnnotationTypeName, nil] containsObject:annotation.annotationType])
{
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];
... ... @@ -1713,14 +1768,17 @@
}
else
{
if (_delegateHasWillHideLayerForAnnotation)
[delegate mapView:self willHideLayerForAnnotation:annotation];
if ( ! [[NSArray arrayWithObjects:kRMUserLocationAnnotationTypeName, kRMAccuracyCircleAnnotationTypeName, kRMTrackingHaloAnnotationTypeName, nil] containsObject:annotation.annotationType])
{
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]);
... ... @@ -1806,10 +1864,13 @@
{
for (RMAnnotation *annotation in annotationsToRemove)
{
[annotations removeObject:annotation];
[visibleAnnotations removeObject:annotation];
[self.quadTree removeAnnotation:annotation];
annotation.layer = nil;
if ( ! [[NSArray arrayWithObjects:kRMUserLocationAnnotationTypeName, kRMAccuracyCircleAnnotationTypeName, kRMTrackingHaloAnnotationTypeName, nil] containsObject:annotation.annotationType])
{
[annotations removeObject:annotation];
[visibleAnnotations removeObject:annotation];
[self.quadTree removeAnnotation:annotation];
annotation.layer = nil;
}
}
}
... ... @@ -1818,25 +1879,360 @@
- (void)removeAllAnnotations
{
@synchronized (annotations)
[self removeAnnotations:[annotations allObjects]];
}
- (CGPoint)mapPositionForAnnotation:(RMAnnotation *)annotation
{
[self correctScreenPosition:annotation];
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];
for (RMAnnotation *annotation in annotations)
if ([[NSArray arrayWithObjects:kRMUserLocationAnnotationTypeName, kRMAccuracyCircleAnnotationTypeName, kRMTrackingHaloAnnotationTypeName, nil] containsObject:annotation.annotationType])
[self removeAnnotation:annotation];
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; }
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; }
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];
[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];
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];
((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"]];
[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)
{
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationCurveEaseInOut
animations:^(void) { mapScrollView.transform = CGAffineTransformMakeRotation((M_PI / -180) * newHeading.trueHeading); }
completion:nil];
}
}
- (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>
... ... @@ -66,4 +73,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
... ...
... ... @@ -72,8 +72,6 @@ typedef enum {
- (void)addAnnotations:(NSArray *)annotations;
- (void)removeAnnotation:(RMAnnotation *)annotation;
- (void)removeAllObjects;
// Returns all annotations that are either inside of or intersect with boundingBox
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox;
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox
... ...
... ... @@ -618,15 +618,6 @@
}
}
- (void)removeAllObjects
{
@synchronized (self)
{
[rootNode release];
rootNode = [[RMQuadTreeNode alloc] initWithMapView:mapView forParent:nil inBoundingBox:[[RMProjection googleProjection] planetBounds]];
}
}
#pragma mark -
- (NSArray *)annotationsInProjectedRect:(RMProjectedRect)boundingBox
... ...
//
// 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])
{
[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"];
}
}
@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
... ...
... ... @@ -155,7 +155,11 @@
DD6380DE152E72880074E66E /* RMMapBoxSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD6380DC152E72880074E66E /* RMMapBoxSource.m */; };
DD8CDB4A14E0507100B73EB9 /* RMMapQuestOSMSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8CDB4814E0507100B73EB9 /* RMMapQuestOSMSource.h */; };
DD8CDB4B14E0507100B73EB9 /* RMMapQuestOSMSource.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */; };
DD8FD7541559E4A40044D96F /* RMUserLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8FD7521559E4A40044D96F /* RMUserLocation.h */; };
DD8FD7551559E4A40044D96F /* RMUserLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8FD7531559E4A40044D96F /* RMUserLocation.m */; };
DD96559215264C810008517A /* RMMapBoxSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DD96559015264C810008517A /* RMMapBoxSource.h */; };
DDA6B8BD155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA6B8BB155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h */; };
DDA6B8BE155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B8BC155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m */; };
DDC4BED5152E3BD700089409 /* RMInteractiveSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DDC4BED3152E3BD700089409 /* RMInteractiveSource.h */; };
DDC4BEE3152E3DE700089409 /* GRMustache.h in Headers */ = {isa = PBXBuildFile; fileRef = DDC4BED8152E3DE700089409 /* GRMustache.h */; };
DDC4BEE4152E3DE700089409 /* GRMustacheAvailabilityMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = DDC4BED9152E3DE700089409 /* GRMustacheAvailabilityMacros.h */; };
... ... @@ -321,7 +325,21 @@
DD6380DC152E72880074E66E /* RMMapBoxSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapBoxSource.m; sourceTree = "<group>"; };
DD8CDB4814E0507100B73EB9 /* RMMapQuestOSMSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapQuestOSMSource.h; sourceTree = "<group>"; };
DD8CDB4914E0507100B73EB9 /* RMMapQuestOSMSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMMapQuestOSMSource.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>"; };
DD96559015264C810008517A /* RMMapBoxSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMMapBoxSource.h; 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>"; };
DDC4BED3152E3BD700089409 /* RMInteractiveSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RMInteractiveSource.h; sourceTree = "<group>"; };
DDC4BED4152E3BD700089409 /* RMInteractiveSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RMInteractiveSource.m; sourceTree = "<group>"; };
DDC4BED8152E3DE700089409 /* GRMustache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRMustache.h; sourceTree = "<group>"; };
... ... @@ -638,6 +656,7 @@
B83E64EB0E80E73F001663B6 /* Tile Source */,
B83E64CE0E80E73F001663B6 /* Tile Layer & Overlay */,
B86F26A80E8742ED007A3773 /* Markers and other layers */,
DD8FD7571559ED930044D96F /* User Location */,
1266929E0EB75BEA00E002D5 /* Configuration */,
B8474B8C0EB40094006A0BC1 /* FMDB */,
B8474C610EB53A01006A0BC1 /* Resources */,
... ... @@ -658,6 +677,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>";
};
DDC4BED1152E3BC200089409 /* Interactivity */ = {
isa = PBXGroup;
children = (
... ... @@ -756,6 +804,8 @@
DDC4BEEC152E3DE700089409 /* GRMustacheVersion.h in Headers */,
DD6380DD152E72880074E66E /* RMMapBoxSource.h in Headers */,
DD103E241540E3CF00AA65DD /* RMCompositeSource.h in Headers */,
DD8FD7541559E4A40044D96F /* RMUserLocation.h in Headers */,
DDA6B8BD155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
... ... @@ -975,6 +1025,8 @@
DD6380DE152E72880074E66E /* RMMapBoxSource.m in Sources */,
DDC4BEF2152E3FAE00089409 /* RMInteractiveSource.m in Sources */,
DD103E251540E3CF00AA65DD /* RMCompositeSource.m in Sources */,
DD8FD7551559E4A40044D96F /* RMUserLocation.m in Sources */,
DDA6B8BE155CAB67003DB5D8 /* RMUserTrackingBarButtonItem.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
... ...