Skip to content

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 viewDidLoadmethod of the ViewController:

#import "LocationManagerShared.h"
...

@implementation ViewController

...

- (void)viewDidLoad {
  ...
  [[LocationManagerShared sharedManager] startUpdatingLocation];
}

Notifying 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 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 series. We create a class NavigationLoop with a method run which is called every second to update our navigation state.

#import "NavigationLoop.h"

@implementation NavigationLoop

- (void)run:(NSTimer*)theTimer {  
    [self _run];
}

- (void) _run {
  // here we will manipulate our mapView
}

@end

Here is the NavigationLoop.h file:

#import <Foundation/Foundation.h>
#import "navicoreFramework/NSDK_Navigation.h"


@interface NavigationLoop : NSObject

-(void)run:(NSTimer*)theTimer;

@end

To avoid updating our ViewController or our UIMapView directly we also introduce a new interface called NavigationControl. Currently it will consist only of one callback function named onGPS which the NavigationLoop will call every run.

@protocol NavigationControl <NSObject>
- (void) onGPS:(NSDK_GPSData*) gps;
@end

And we update our NavigationLoop class to store references to NavigationControl objects

@interface NavigationLoop () {
}
@property NSMutableArray<id<NavigationControl>> *navigationControls;
@end


@implementation NavigationLoop
- (instancetype)init {
    self = [super init];
    if (self) {
        _navigationControls = [NSMutableArray array];
    }
    return self;
}


-(void) addControl:(id<NavigationControl>) control {
    [_navigationControls addObject:control];
}
...
@end

In our ViewController we call the [NavigationLoop addControl] method with the _uiMapView object. And we call the run method every second 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] init];
  [_navigationLoop addControl:_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 NSTimer 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

To update our position in the map we implement the NavigationControl Interface in our UIMapView class and update the center with the given GPSData

#import "navicoreFramework/NSDK_Navigation.h"
#import "NavigationControl.h"

@interface UIMapView : NSDK_UIMapView<NavigationControl>
@end
@implementation UIMapView
...
- (void)onGPS:(NSDK_GPSData *)nsdk_gpsdata {   
    [self setCenterWithPosition:[nsdk_gpsdata getGPSPositionMerc]];
    [self refresh];
}

Now we can call the onGPS methood in the NavigationLoop. To avoid setting the map to a bogus position, we check if NSDK_GPSData has a Fix with the getFix method.

@implementation NavigationLoop
...

- (void) _run {
    NSDK_GPSData * nsdk_gpsdata = [[NSDK_GPSManager sharedInstance] getCurrentPosition:nil];
    if (nsdk_gpsdata != nil && [nsdk_gpsdata getFix] >= 0) {
        for (id navigationControl in _navigationControls) {
            [navigationControl onGPS:nsdk_gpsdata];
        }
    }
}
@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
round map marker directed map marker
MapMarkerStyleRound MapMarkerStyleDirected

We just set the MapMarker with the mercator position, the orientation(course) of the NSDK_GPSData and the desired style. The improved onGPS method looks like this:

@implementation UIMapView
...
# pragma mark NavigationControl implementation
- (void)onGPS:(NSDK_GPSData *)nsdk_gpsdata {

    NSDK_MapMarkerStyle mapMarkerStyle = MapMarkerStyleDirected;
    [self setMapMarkerWithCenter:[nsdk_gpsdata getGPSPositionMerc] orientation:[nsdk_gpsdata getCourse] northernAdjust:NO style:mapMarkerStyle];
    [self setCenterWithPosition:[nsdk_gpsdata getGPSPositionMerc]];
    [self refresh];
}

Extended CCP drawing

You have two options if you want to use your own CCP image: 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 override the refresh method of the NSDK_UIMapView class in our UIMapView class:

#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];
        }
    }
}