Hacking Mapbox Unity SDK to Get Sub-Meter Coordinates Accuracy in iOS

May 11th, 2018

AR is a particularly interesting technology when combined with geospatial. AR provides an intuitive interface to navigate and understand the world in 3D.

Some geospatial applications need precision. Consumer phones and tablets’ GPS units are not particularly accurate, so this is often not a concern. If we encode latitude and longitude data using single precision floats, we can expect to be off. How off?

import struct
coord = 90.123456789
coord_float = struct.unpack('f', struct.pack('f', coord))[0] # 90.12345886230469

# East/West at 45 N/S https://en.wikipedia.org/wiki/Decimal_degrees (it gets worse at the equator)
dd_to_meters = 787100 

abs(coord - coord_float) * dd_to_meters
# 1.6318981177747105 (meters)

So if we need sub-meter accuracy, we need to exercise care.

Mapbox Unity SDK makes it easy to get started with geospatial applications using ARKit and ARCore. Using ARKit, let’s check our tech stack to see if we have the precision we need:

UnitySetLastLocation(double timestamp,
    float latitude,
    float longitude,
    float altitude,
    float horizontalAccuracy,
    float verticalAccuracy);

Uups! Not good.

Mapbox developers seem to have noticed the problem, but went ahead with:

//_currentLocation.LatitudeLongitude = new Vector2d(lastData.latitude, lastData.longitude);
// HACK to get back to double precision, does this even work?
// https://forum.unity.com/threads/precision-of-location-longitude-is-worse-when-longitude-is-beyond-100-degrees.133192/#post-1835164
double latitude = double.Parse(lastData.latitude.ToString("R", invariantCulture), invariantCulture);
double longitude = double.Parse(lastData.longitude.ToString("R", invariantCulture), invariantCulture);
_currentLocation.LatitudeLongitude = new Vector2d(latitude, longitude);

The answer to the comment is no. The loss of precision has already happened during UnitySetLastLocation, so converting to string will just burn a few CPU cycles.

A Unity engineer correctly points out:

A feedback request is opened, but several years later the problem is still here: https://feedback.unity3d.com/suggestions/gps-accuracy-double

A workaround is proposed.

 

Seems a reasonable workaround, but this way we need to deal with message passing and substantially modify Mapbox’s SDK.

We can’t easily modify Unity’s API. Let’s look at the UnitySetLastLocation function signature…

UnitySetLastLocation(double timestamp,
    float latitude,
    float longitude,
    float altitude,
    float horizontalAccuracy,
    float verticalAccuracy);

Comparing Mapbox’s DeviceLocationProvider.cs code, we notice that neither the altitude or verticalAccuracy parameters are being used. Free 64 bits. Just what we need…

The Plan

  1. Break each coordinate (CLLocationDegrees) in two float variables via pointer arithmetic.
  2. Send them back to Unity using the extra altitude and verticalAccuracy parameters (which Mapbox doesn’t use).
  3. Combine them back together.

We modify iPhone_Sensors.mm as follows:

@implementation LocationServiceDelegate

- (void)locationManager:(CLLocationManager*)manager didUpdateLocations:(NSArray*)locations
{
    CLLocation* lastLocation = locations.lastObject;
    
    gLocationServiceStatus.locationStatus = kLocationServiceRunning;
    
    double latitude = lastLocation.coordinate.latitude;
    double longitude = lastLocation.coordinate.longitude;
    
    float *plat = (float *)&latitude;
    float *plon = (float *)&longitude;
    
    UnitySetLastLocation([lastLocation.timestamp timeIntervalSince1970],
                         *plat,
                         *plon,
                         *(plon + 1),
                         lastLocation.horizontalAccuracy,
                         *(plat + 1));
    
    //    UnitySetLastLocation([lastLocation.timestamp timeIntervalSince1970],
    //        lastLocation.coordinate.latitude, lastLocation.coordinate.longitude, lastLocation.altitude,
    //        lastLocation.horizontalAccuracy, lastLocation.verticalAccuracy
    //        );
}

Then we modify Mapbox’s DeviceLocationProvider.cs:

  class PreciseLocationInfo {
        //
        // Summary:
        //     Geographical device location latitude.
        public double latitude;
        //
        // Summary:
        //     Geographical device location latitude.
        public double longitude;
        //
        // Summary:
        //     Horizontal accuracy of the location.
        public float horizontalAccuracy;
        //
        // Summary:
        //     Timestamp (in seconds since 1970) when location was last time updated.
        public double timestamp;

        public static PreciseLocationInfo FromPackedLocationInfo(LocationInfo packedInfo) {
            double latitude;
            double longitude;

            unsafe {
                float* plat = (float*)&latitude;
                *plat = packedInfo.latitude;
                *(plat + 1) = packedInfo.verticalAccuracy;

                float* plon = (float*)&longitude;
                *plon = packedInfo.longitude;
                *(plon + 1) = packedInfo.altitude;
            }

            var p = new PreciseLocationInfo();
            p.timestamp = packedInfo.timestamp;
            p.longitude = longitude;
            p.latitude = latitude;
            p.horizontalAccuracy = packedInfo.horizontalAccuracy;
            return p;
        }
    };

// .......

#if UNITY_IOS
                var lastData = PreciseLocationInfo.FromPackedLocationInfo(Input.location.lastData);
#else
                var lastData = Input.location.lastData;
#endif

And we get double precision, waiting for Unity to fix this.