How To Handle Incorrect CLLocation Data - ios

I'm developing a fitness tracking app like Runtastic, Nike+ etc. I'll be mapping the entire activity from start to finish. I don't start updating location changes when the app launches, instead I'm starting when the user starts a workout. But when the locationManager start updating, first 3 to 5 CLLocations are very incorrect. Up to a kilometer in errors. I'm using the following code to initialize the location manager:
self.locationManager = [(PFAppDelegate *)[[UIApplication sharedApplication] delegate] locationManager];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
self.locationManager.distanceFilter = 5.0;
[self.locationManager startUpdatingLocation];
And in the locationManagerDidUpdateToLocation method:
if( didFindGPS == NO )
{
if( [[lastLocation timestamp] timeIntervalSinceNow] < 10.0 )
{
if( [lastLocation horizontalAccuracy] < 20)
{
didFindGPS = YES;
}
}
}
else
{
//process data here
}
This doesn't filter out those first incorrect locations. I've also tried ignoring locations that have horizontalAccury values that are smaller than 20, but then the app doesn't process any locations.
What can be done to improve the first locations or to handle the first incorrect ones?

Change
if ([lastLocation horizontalAccuracy] < 20)...
to
if (([lastLocation horizontalAccuracy] > 0) && ([lastLocation horizontalAccuracy] < 20))...
According to documentation
A negative value indicates that the location’s latitude and longitude
are invalid.
A negative value of horizontalAccuracy that is.
If you want to set the condition for processing data (seems like you do) you should rewrite the code to:
if (([lastLocation horizontalAccuracy] > 0) && ([lastLocation horizontalAccuracy] < 20))
{
didFindGPS = YES;
//process the data here... since here the location fits your limitations
//and you don't loose the first location (as in original code)
}
else
{
didFindGPS = NO;
}
Note that this code can give you some false alarms for loosing the GPS so you might want to omit the else block.

Related

Background location service stops working in a period of time

I'm developing an iOS application that users can save their traveling route to server (by posting their locations through API). The issue I am struggling with is several users reported that the route they saved is interrupted in the middle of the trip. In detail, when users review their saved route on map, there is a part of route being just a straight line because somehow locations of that route section are not sent to server or just because the device can not receive locations at that time.
The weird thing is that the rest of route was recorded normally so seems like the location service stopped working for a period of time but after that it started again so my app could record it fine.
And the most frustrating thing is that I can not reproduce this issue.
Here are circumstances of the issue that user reported:
- User started the app then locked device screen and put it in their pocket, they did not touch it in the whole journey. No battery drain or crash happened.
- After driving about 8-9km and everything worked fine, route recording was interrupted in the next ~ 65km, then well-recorded again in the rest ~ 80km.
Below is my project setup:
- Background Modes in ON in Capabilities with Location updates.
- Locations received from location service are filtered based on timestamp and accuracy and saved to core data with a “isSent” flag marking if a location is sent successfully to server. This way my app can cover the case when network connection is down.
- Locations marked with false “isSent” flag will be sent to server every 30 seconds.
My LocationManager code:
class LocationManager: NSObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager = {
var _locationManager = CLLocationManager()
_locationManager.delegate = self
_locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
_locationManager.activityType = .automotiveNavigation
_locationManager.allowsBackgroundLocationUpdates = true
_locationManager.pausesLocationUpdatesAutomatically = false
return _locationManager
}()
func startLocationService() {
locationManager.startUpdatingLocation()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var positionsToSavedToDb: [DbPositionModel] = []
for location in locations {
let howRecent = location.timestamp.timeIntervalSinceNow
guard abs(location.timestamp.timeIntervalSinceNow) < 10 && location.horizontalAccuracy > 0 && location.horizontalAccuracy < 33 else {
continue
}
if self.locations.count > 0 {
let distanceSinceLastLocation = location.distance(from: self.locations.last!)
let timeSinceLastLocation = location.timestamp.timeIntervalSince(self.locations.last!.timestamp)
if (distanceSinceLastLocation < 5 && abs(timeSinceLastLocation) < 10) {
continue
}
}
// Create DbPositionModel from location and append to positionsToSavedToDb
}
// Save positionsToSavedToDb to core data
}
#objc func each30Seconds(_ timer: Timer) {
// Select in database DbPositionModel objects with false “isSent” flag to send to server
}
}
Can you guys help me find out black holes in my code or anything I can do to reproduce / fix this issue? Thanks a lot!!!
Your setup looks fine to me. Just one question. When you say it didn't work for 60km and then it started working for 80km, was that all while in background? I mean the user didn't need to enter foreground for it to start working again did they?
Your limit for location.horizontalAccuracy is 33. You're thinking that it's going to be very accurate. I'm not sure, maybe the device/city are a bad combination, and then you're returning early. I suggest that you log the reason why you exit early. Their city might be different from yours. Also I've heard that the GPS of the iPhoneX even though has the correct location, it returns a high number for its horizontalAccuracy. Is this happening mostly for iPhoneX users?
enum LocationAccuracyError: Error {
case stale(secondsOld: Double)
case invalid
case lowAccuracy(metersOff: Double)
}
extension LocationAccuracyError: LocalizedError{
var errorDescription: String? {
switch self {
case .stale(let seconds):
return NSLocalizedString("location was stale by: \(seconds) seconds", comment: "")
case .invalid:
return NSLocalizedString("location was invalid)", comment: "")
case .lowAccuracy(let metersOff):
return NSLocalizedString("location's horizontal Accuracy was off by likely more than: \(metersOff) meters" , comment: "")
}
}
}
And then have a function like this to check each location.
private func checkLocationAccuracy(from location: CLLocation) throws {
let ageOfLocation = -location.timestamp.timeIntervalSinceNow
if ageOfLocation >= maximumAcceptedStale {
throw LocationAccuracyError.stale(secondsOld: ageOfLocation)
}
if location.horizontalAccuracy <= 0 {
throw LocationAccuracyError.invalid
}
if location.horizontalAccuracy > MaximumAcceptedHorizontalAccuracy{
throw LocationAccuracyError.lowAccuracy(metersOff: location.horizontalAccuracy)
}
}
Your end usage would be like:
do {
try checkLocationAccuracy(from: location)
} catch let error {
writelog("Bad Location: \(error.localizedDescription)")
}
I'd also add logs around your app state as well e.g. add a log to capture didEnterBackground

Location timestamp accuracy in ios

After analysing location services in iOS 10, found that some inconsistency is in the caching behaviour.
Fetching locations in a periodic time (in my case every 20 secs) returns locations but their timestamps are not in chronologically ordered. This indicates that the caching locations might have issues. So if you are checking accuracy through location-timestamp better to save the previous timestamps also. So that you could decide that the location fetched can be used or not.
Below image is taken from my console log. Here I used the format "Lat Long : latitude_longitude | location_timestamp | Now : current_timestamp"
Yes some time in best accuracy ios take the location from the cache so you need to avoid that location here is the code for accurate locationtion.
Update :
"Because it can take several seconds to return an initial location, the location manager typically delivers the previously cached location data immediately and then delivers more up-to-date location data as it becomes available. Therefore it is always a good idea to check the timestamp of any location object before taking any actions."
Reference :
https://developer.apple.com/reference/corelocation/cllocationmanager
Note: you can vary the accuracy for the device like ipod and ipad
//MARK: Location delgates
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
if locations.count > 0
{
let location = locations[locations.count-1]
let maxAge:TimeInterval = 60;
let requiredAccuracy:CLLocationAccuracy = 100;
let locationIsValid:Bool = Date().timeIntervalSince(location.timestamp) < maxAge && location.horizontalAccuracy <= requiredAccuracy;
if locationIsValid
{
NSLog(",,, location : %#",location);
NSLog("valid locations.....");
}
}
}
The problem behind this is that sometimes the timestamps do not match to the location! E.g. while traveling you suddenly records speeds > 300km/h and best accuracy.
I would sort the locations and only would take the last one if not too old:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let sortedLocations = locations.sorted { (l1, l2) -> Bool in
return l1.timestamp.compare(l2.timestamp) != .orderedDescending
}
if let newestLocation = sortedLocations.last{
if Date().timeIntervalSince(newestLocation.timestamp) < 60{
//TODO: Use the location
}
}
}
Yes like #chirag shah commented we definitely need to do that check. My suggestion is we should know about that the caching technique has been modified. And also it is not enough only checking the timestamp, we have to focus on the failure case. Here is the objective C code
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
CLLocation* location = [locations lastObject];
NSDate* locationTimestamp = location.timestamp;
NSTimeInterval cachedBefore = [locationTimestamp timeIntervalSinceNow];
if (fabs(cachedBefore) < 60.0) {
// Implement your code here
}else{
// Try again or wait for fetching location
}
}

GPS Accuracy Resetting on its own on iOS

In an iPhone app that I'm building, GPS accuracy is a user-controlled value that can be set to either KCLLocationAccuracyNearestTenMeters or KCLLocationAccuracyHundred Meters. The code works fine to capture a track, but when monitoring the app in the Location Energy Impact Instrument I’m seeing some unexpected behavior. When accuracy is set to nearest 10 meters the app works as expected, however, if it’s set to 100 meters the setting remains OK for between a half and two seconds, as shown by the Instrument, but then it switches to KCLLocationAccuracy Best. This setting isn't an option in the app, or even a String anywhere in the code.
One twist here, I am running this as Swift 3.0 using Xcode 8, but it's working as it did before and the new platform doesn't seem to be an issue (and shouldn't have this kind of impact). It does mean that some API's have changed and may look different (and to my taste, better) below.
Obviously, this resetting has a very counterproductive impact on energy consumption, which shows in the Instrument which goes from Low to High energy usage at the point where it switches. I can't identify any point where the app would be doing this unintentionally - in fact, during this one-second period it should only be appending points returned by didUpdateLocations to a pending buffer for later processing, as shown below.
In a shared constants declaration:
let defaultTrackingAccuracy = kCLLocationAccuracyNearestTenMeters
let alternateTrackingAccuracy = kCLLocationAccuracyHundredMeters
var trackingAccuracy = kCLLocationAccuracyNearestTenMeters
var waypointInterval = 100
In a settings ViewController (can also reset waypointInterval but did not):
#IBAction func accuracySwitched(_ sender: UISwitch) {
if accuracySwitch.isOn {
Set.shared.trackingAccuracy = Set.shared.defaultTrackingAccuracy
} else {
Set.shared.trackingAccuracy = Set.shared.alternateTrackingAccuracy
}
}
LocationManager instantiation in a Model portion of code:
lazy var locationManager: CLLocationManager = {
var _locationManager = CLLocationManager()
_locationManager.delegate = self
_locationManager.desiredAccuracy = Set.shared.trackingAccuracy
_locationManager.allowsBackgroundLocationUpdates = true
_locationManager.distanceFilter = Double(Set.shared.waypointInterval)
return _locationManager
}()
Starting the location manager updates in the Model:
func setupLocationManager()
{
if CLLocationManager.authorizationStatus() != .authorizedAlways {
locationManager.requestAlwaysAuthorization()
if CLLocationManager.authorizationStatus() != .authorizedAlways {
delegate?.displayNotice("Unable to Capture Track", alertMessage: "This device requires authorization to use location services in order to capture a track. \n\nPress Continue to return to the Track List.", buttonText: "Continue")
}
}
if !CLLocationManager.locationServicesEnabled() {
delegate?.abortTrackCapture(.gpsUnavailable)
}
locationManager.startUpdatingLocation()
}
Processing points returned by the location manager instance, in the same Model portion:
#objc(locationManager:didUpdateLocations:) func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
assert(locations.first != nil, "didUpdateLocations called with empty array.")
if recordStatus == .record {
pendingPointsBuffer = pendingPointsBuffer + locations
}
lastPosition.latitude = locations.last!.coordinate.latitude
lastPosition.longitude = locations.last!.coordinate.longitude
lastPosition.elevation = locations.last!.altitude
}
Every few seconds, there is a routine that takes points from the buffer and posts them into a CoreData database, but that only accesses the buffer - it has no interaction with the location manager (so it can sleep through periods when the app is in background while the code above keeps loading points into the buffer).
The user interface is in a separate ViewController module, but shouldn’t be doing anything during the two seconds in question.
I'm thinking that there may be some OS based setting or process that could be doing this, but I haven't found anything that seems to do that.
Thanks for your input - any thoughts are appreciated.

CLLocationManager always returning speed: -1

I'm trying to determine the location of my users and I have everything setup properly but my devices return -1 for speed and course all the time. When I run the exact same setup on the simulator speed and course values are just fine.
I have calibrated the compass on my test devices and the maps show the correct locations. I have also reset location and privacy settings and I have used 5 different phones. It's always the same story on all devices.
manager = [[CLLocationManager alloc] init];
self.manager.delegate = self;
manager.desiredAccuracy = kCLLocationAccuracyBest;
manager.distanceFilter = 500;
manager.activityType = CLActivityTypeOther;
[manager startUpdatingLocation];
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
for (CLLocation *location in locations) {
//double speed = 0.0;
//speed = location.speed;
//NSLog(#"%f", s);
NSLog(#"%#", location.description);
}
}
This returns this:
2015-05-07 12:16:57.703 Health_[323:84932] Location: (
"<+52.55719238,+11.02049967> +/- 165.00m (speed -1.00 mps / course -1.00) # 07.05.15 12:16:57 Mitteleurop\U00e4ische Sommerzeit"
)
Does anyone have an idea what's going on?
As it turns out CLLocationManager does not return any speed or course information if you
a. don't move at all. A fixed location always has -1 values (should return something else in my opinion but oh well).
b. haven't let the location manager run for a little while. If you stop the location manager immediately after you have created and started it, it does not yet know any of these information (again should return something different in my opinion then "invalid value").
In my case I was doing both of these things and as it turns out none of my devices are broken... (phew)
I made a little application and put it on Github to help others understand this problem. Just put it on your phone and move around for a little while. It will eventually return something but just not immediately and not if you don't move and haven't moved in a while.

Best Reactive-Cocoa approach for writing a CLLocationManagerDelegate, which will infrequently fetch location

Background
I'm really excited by the ReactiveCocoa framework and the potential it has, so I've decided that I'm going to bite the bullet and write my first app using it.
In my app, I've already written the various services and delegates, but I now need to 'Reactive-Cocoa-ise' them so that I can get on with actual GUI side of things.
That said, so that I better understand this, I'm writing a simple bit of code just to try out concepts.
In this case, writing a wrapper for CLLocationManagerDelegate.
In the actual app, the use case would be this:
1) When the app is loaded up (viewDidLoad) then 2) Attempt to fetch
the location of the device by
2.1) if location services not enabled then
2.1.1) check authorisation status, and if allowed to startMonitoringSignificantLocationChanges,
2.1.2) else return an error
2.2) else (location services are enabled)
2.2.1) if the location manager last location was 'recent' (6 hours or less) return that
2.2.2) else startMonitoringSignificantLocationChanges
3) when returning the location (either straight away, or via
locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations), then we also stopMonitoringSignificantLocationChanges
4) If code that calls on the LocationManagerDelegate receives and error, then ask the delegate for a fixed 'default' value
5) Next piece of code then uses the location (either fetched, or default) to go off and do a bunch of calls on third party services (calls to WebServices etc)
You can see a test harness at https://github.com/ippoippo/reactive-core-location-test
Concerns
The harness 'works' in that it goes off and fetches the location. However, I'm really concerned I'm doing this in the right way.
Consider this code
RACSignal *fetchLocationSignal = [lmDelegate latestLocationSignal];
RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
NSLog(#"Unable to fetch location, going to grab default instead: %#", error);
CLLocation *defaultLocation = [lmDelegate defaultLocation];
NSLog(#"defaultLocation = [%#]", defaultLocation);
// TODO OK, so now what. I just want to handle the error and
// pass back this default location as if nothing went wrong???
return [RACSignal empty];
}];
NSLog(#"Created signal, about to bind on self.locationLabel text");
RAC(self.locationLabel, text) = [[locationSignal filter:^BOOL(CLLocation *newLocation) {
NSLog(#"Doing filter first, newLocation = [%#]", newLocation);
return newLocation != nil;
}] map:^(CLLocation *newLocation) {
NSLog(#"Going to return the coordinates in map function");
return [NSString stringWithFormat:#"%d, %d", newLocation.coordinate.longitude, newLocation.coordinate.latitude];
}];
Questions
1) How do I handle errors? I can use catch which then gives me the opportunity to then ask the delegate for a default location. But, I'm then stuck on how to then pass back that default location as a signal? Or do I just change my defaultLocation method to return a RACSignal rather than CLLocation??
2) When I return the location, should it be done as sendNext or sendCompleted? Currently it's coded as sendNext, but it seems like something that would be done as sendCompleted.
3) Actually, does the answer to that depend on how I create the Delegate. This test app creates a new Delegate each time the view is loaded. Is that something I should do, I should I make the Delegate a singleton. If it's a singleton, then sendNext seems the right thing to do. But if would then imply that I move the code that I have in latestLocationSignal in my delegate into an init method instead?
Apologies for the rambling question(s), just seem to be going around in circles in my head.
Updating the answer above to ReactiveCocoa 4 and Swift 2.1:
import Foundation
import ReactiveCocoa
class SignalCollector: NSObject, CLLocationManagerDelegate {
let (signal,sink) = Signal<CLLocation, NoError>.pipe()
let locationManager = CLLocationManager()
func start(){
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for item in locations {
guard let location = item as CLLocation! else { return }
sink.sendNext(location)
}
}
And to listen the events:
var locationSignals : [CLLocation] = []
signal.observeNext({ newLocation in
locationSignals.append(newLocation)
})
How do I handle errors? I can use catch which then gives me the opportunity to then ask the delegate for a default location.
You're so close. Use +[RACSignal return:] to create a signal which just sends your default location:
RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
NSLog(#"Unable to fetch location, going to grab default instead: %#", error);
CLLocation *defaultLocation = [lmDelegate defaultLocation];
NSLog(#"defaultLocation = [%#]", defaultLocation);
return [RACSignal return:defaultLocation];
}];
When I return the location, should it be done as sendNext or sendCompleted? Currently it's coded as sendNext, but it seems like something that would be done as sendCompleted.
In order to actually send data, you'll need to use -sendNext:. If that's the only thing that the signal is sending, then it should also -sendCompleted after that. But if it's going to continue to send the location as accuracy improves or location changes, then it shouldn't complete yet.
More generally, signals can send any numbers of nexts (0*) but can only complete or error. Once completion or error is sent, the signal's done. It won't send any more values.
Actually, does the answer to that depend on how I create the Delegate. This test app creates a new Delegate each time the view is loaded. Is that something I should do, I should I make the Delegate a singleton. If it's a singleton, then sendNext seems the right thing to do. But if would then imply that I move the code that I have in latestLocationSignal in my delegate into an init method instead?
I'm not sure I have enough context to answer this, except to say that singletons are never the answer ;) Creating a new delegate each time the view is loaded seems reasonable to me.
It's hard to answer broad questions like this because I don't know your design nearly as well as you do. Hopefully these answers help a bit. If you need more clarification, specific examples are really helpful.
Seems to be super easy in ReactiveCocoa 3.0 and swift :
import ReactiveCocoa
import LlamaKit
class SignalCollector: NSObject, CLLocationManagerDelegate {
let (signal,sink) = Signal<CLLocation, NoError>.pipe()
let locationManager = CLLocationManager()
func start(){
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
for item in locations {
if let location = item as? CLLocation {
sink.put(Event.Next(Box(location))
}
}
}
}
And wherever you need to listen to the location events, use the signal with an observer :
var locationSignals : [CLLocation] = []
signal.observe(next: { value in locationSignals.append(value)})
Note that since it's pre-alpha at the moment the syntax is likely to change.

Resources