03 Current Position
Overview
You will learn how to use the iOS Location Service to acquire the current car position and strategies to display it on the map.
Base
Use Tutorial02
as a starting point for this project.
Requesting Location Updates
To get an overview of how to get the user location, please read the Location and Maps Programming Guide provided by Apple.
First, open "Info.plist" and add the following key: Privacy - Location When In Use Usage Description
. Set a value for this key. This value is shown to the user, when the iOS runtime asks for permission to retrieve the current GPS location.
We create a new cocoa class LocationManagerShared
, which encapsulates the logic for retrieving the location.
The file LocationManagerShared.h
looks like this:
#import <Foundation/Foundation.h> #import <CoreLocation/CoreLocation.h> #import "navicoreFramework/NSDK_Navigation.h" @interface LocationManagerShared : NSObject <CLLocationManagerDelegate> + (LocationManagerShared *) sharedManager; - (void) startUpdatingLocation; - (void) stopUpdatingLocation; @end
This class is implemented as a singleton and implements the CLLocationManagerDelegate protocol. Don't forget to include the Header #import <CoreLocation/CoreLocation.h>
The implementation file LocationManagerShared.m
looks like this:
#import "LocationManagerShared.h" @interface LocationManagerShared () @property (strong, nonatomic) CLLocationManager *cllocationManager; @end @implementation LocationManagerShared + (LocationManagerShared *)sharedManager { static LocationManagerShared *sharedManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedManager = [[self alloc] init]; }); return sharedManager; } - (id)init { self = [super init]; if (self) { _cllocationManager = [[CLLocationManager alloc]init]; _cllocationManager.delegate = self; _cllocationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation; [self.cllocationManager requestWhenInUseAuthorization]; } return self; } //CLLocationManagerDelegate protocol fct. - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { CLLocation * location = locations.lastObject; NSLog(@"LocationManagerShared didUpdateLocation horizontalAccuracy:%f course:%f", location.horizontalAccuracy, location.course); } //CLLocationManagerDelegate protocol fct. - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { NSLog(@"LocationManagerShared didFailWithError called"); } - (void) startUpdatingLocation { [_cllocationManager startUpdatingLocation]; } - (void) stopUpdatingLocation { [_cllocationManager stopUpdatingLocation]; } @end
Please note, that Apple lets you choose between different accuracies for the retrieved location. For a navigation App, _cllocationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
should be the best option. At this stage, we just log the retrieved location.
For starting the location retrieval, add the following line into the viewDidLoad
method of the ViewController:
#import "LocationManagerShared.h" ... @implementation ViewController ... - (void)viewDidLoad { ... [[LocationManagerShared sharedManager] startUpdatingLocation]; }
Notify the SDK
This would be enough to get the current position and show it on the NSDK_UIMapView. But we also need to notify the SDK about the location updates so that we can use the position in a navigation for example. For this reason, we have the NSDK_GPSManager singleton in the NavigationSDK. We need to initialize it by calling openGPSDevice:
. Our new startUpdatingLocation and stopUpdatingLocation methods look like this:
@implementation LocationManagerShared ... - (void) startUpdatingLocation { NSError * error; [[NSDK_GPSManager sharedInstance] openGPSDevice:NO error:&error]; [_cllocationManager startUpdatingLocation]; } - (void) stopUpdatingLocation { NSError * error; [[NSDK_GPSManager sharedInstance] closeGPSDevice:&error]; [_cllocationManager stopUpdatingLocation]; }
... now we need to push the location we are retrieving in didUpdateLocations:
to the SDK.
//CLLocationManagerDelegate protocol fct. - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { CLLocation * location = locations.lastObject; [self pushLocationInSDK:location]; } - (void) pushLocationInSDK:(CLLocation*) location { [[NSDK_GPSManager sharedInstance] pushLocation:location error:nil]; }
It is necessary to close the device in dealloc
of our ViewController
.
- (void) dealloc { [[LocationManagerShared sharedManager] stopUpdatingLocation]; }
The NavigationLoop
To update our mapView with the current position we implement one of the core mechanisms of this Tutorial. We create a class NavigationLoop
with a method run
which is called every second to update our navigation state.
#import "NavigationLoop.h" #import "UIMapView.h" @interface NavigationLoop () { } @property UIMapView* uiMapView; @end @implementation NavigationLoop - (instancetype)initWithMapView:(UIMapView*) mapView { self = [super init]; if (self) { _uiMapView = mapView; } return self; } - (void)run:(NSTimer*)theTimer { [self _run]; } - (void) _run { // here we will manipulate our mapView } @end
... and here is the NavigationLoop.h file:
#import <Foundation/Foundation.h> #import "navicoreFramework/NSDK_Navigation.h" @interface NavigationLoop : NSObject -(instancetype)initWithMapView:(NSDK_UIMapView*) mapView; -(void)run:(NSTimer*)theTimer; @end
We call the run
method every second by our ViewController by implementing a NSTimer mechanism like this:
#import "NavigationLoop.h" ... @interface ViewController () @property NavigationLoop *navigationLoop; @property NSTimer *navigationLoopTimer; @end @implementation ViewController ... - (void)viewDidLoad { ... _navigationLoop = [[NavigationLoop alloc] initWithMapView:_uiMapView]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; _navigationLoopTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:_navigationLoop selector:@selector(run:) userInfo:nil repeats:YES]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [_navigationLoopTimer invalidate]; _navigationLoopTimer = nil; } ...
Please note, that we put the logic in viewDidAppear
(and not in viewDidLoad
), because our mapView should be fully initialized, when we update the mapView within the NavigationLoop.
Update the map position
Now we can set the mapView position in the NavigationLoop to the current gps position. To avoid setting the map to a bogus position we check if NSDK_GPSData has a Fix with getFix
before setting the mapView center.
@implementation NavigationLoop ... - (void) _run { NSDK_GPSData * nsdk_gpsdata = [[NSDK_GPSManager sharedInstance] getCurrentPosition:nil]; if (nsdk_gpsdata != nil && [nsdk_gpsdata getFix] >= 0) { [_uiMapView setCenterWithPosition:[nsdk_gpsdata getGPSPositionMerc]]; [_uiMapView refresh]; } } @end
Draw the CCP
The easiest way to draw the current car position (CCP) is to use [NSDK_MapView setMapMarkerWithCenter:]
. It comes in two styles:
MapMarkerStyles | |
---|---|
MapMarkerStyleRound |
MapMarkerStyleDirected |
We just set the MapMarker with the mercator position, the orientation(course) of the NSDK_GPSData and the desired style.
@implementation NavigationLoop ... - (void) _run { NSDK_GPSData * nsdk_gpsdata = [[NSDK_GPSManager sharedInstance] getCurrentPosition:nil]; if (nsdk_gpsdata != nil && [nsdk_gpsdata getFix] >= 0) { [_uiMapView setMapMarkerWithCenter:[nsdk_gpsdata getGPSPositionMerc] orientation:[nsdk_gpsdata getCourse] northernAdjust:NO style:MapMarkerStyleDirected]; [_uiMapView setCenterWithPosition:[nsdk_gpsdata getGPSPositionMerc]]; [_uiMapView refresh]; } } @end
Extended CCP drawing
If you wan't to use your own CCP image you have two options. First you can use [NSDK_Navigation addImage:]
and [NSDK_Navigation positionImage:]
to set an image to a desired position at the map. The image has to be in your iOS application bundle. Drawback of this approach is that you cannot alter the orientation of the CCP.
// no ccp orientation int id = [NSDK_Navigation addImage:@"pin.png" center:[searchResult getPosition] error:nil]; [NSDK_Navigation positionImage:id center: [searchResult getPosition] error:nil];
Second you can use [NSDK_UIMapView transformCoordinatesWithSourceFormat:sourcePosition:destinationFormat:error:]
to transform a NSDK_GeoPosition or NSDK_Position to a pixel coordinate on the mapView and then override the refresh
method of NSDK_UIMapView to visualize your own CPP on the mapView.
For this purpose you can create a UIMapView class which subclasses 'NSDK_UIMapView', connect the Map view in the InterfaceBuilder with the new class and override the refresh method:
#import "UIMapView.h" ... @implementation UIMapView ... - (void) refresh { [super refresh]; NSError * err; NSDK_GPSData * gps = [[NSDK_GPSManager sharedInstance] getCurrentPosition:&err]; if (err == nil && [gps getFix] >=0) { NSDK_Position* pixelPos = [self transformCoordinatesWithSourceFormat:NSDK_CS_MERCATOR sourcePosition:[gps getGPSPositionMerc] destinationFormat:NSDK_CS_PIXEL error:&err ]; if (!err) { if (_customCCPImageView == nil) { UIImage * img = [UIImage imageNamed:@"ccp"]; _customCCPImageView = [[UIImageView alloc] initWithImage:img]; } else { [_customCCPImageView removeFromSuperview]; } int width = _customCCPImageView.image.size.width; _customCCPImageView.transform = CGAffineTransformIdentity; [_customCCPImageView setFrame:CGRectMake([pixelPos getX]-width/2, [pixelPos getY]-width/2, width, width)]; CGFloat degrees = [self getOrientation:nil] - [gps getCourse] + 90; CGFloat radians = degrees * M_PI/180; _customCCPImageView.transform = CGAffineTransformMakeRotation(radians); [self.mapImageView addSubview:_customCCPImageView]; } } }