Creating a GPS configuration provider for ASP.NET Core 1.0

Datetime:2016-08-22 22:05:04          Topic: ASP.NET           Share

Turing GPS-points into a progress bar

August 20, 2016

In a previous post, I demonstrated creating a Twitter configuration provider for ASP.NET Monster's #SummerOfConfig contest. This post is a follow-up demonstrating how to create a provider leveraging GPS coordinates to create a unique type of progress bar. Source code available on GitHub .

Provider ingredients

There are two parts to make the provider work. First, we have the logic contained within the provider itself. This stores our start and end-points as well as calculating progress. Second, we have a browser-client for pushing a third-point which is used to determine how close we are to either the start or end-points.

Backend

The provider is a little different than most because we not only need to retrieve data from the provider, we also need to push updates to it. To accomplish this, the provider defines an action that takes coordinates. The action then updates the dictionary used by the provider for read-purposes. Additionally, we need to create a controller-action which accepts coordinates and calls this action.

Creating the source

The configuration workflow relies on coordinates provided by an external configuration source. This frees the provider from having to concern itself with where the raw data comes from. Additionally, the provider can update itself once the related coordinate config. is updated. This is accomplished by registering a callback on the external configuration.

public class GpsProgressConfigurationSource : IConfigurationSource
{
    public GpsProgressConfigurationSource(
        IConfigurationRoot configurationRoot,
        string latitudeStartKey,
        string longitudeStartKey,
        string latitudeEndKey,
        string longitudeEndKey,
        out Action<double, double, string> positionChanged)
    {
        // Initialize the provider.
        updateSettings(configurationRoot, latitudeStartKey, longitudeStartKey, latitudeEndKey, longitudeEndKey);

        // Define what the action does when called by the app.
        positionChanged = (latitude, longitude, accuracy) =>
        {
            data["gps.distance.progress"] = configurationProvider.GetProgress(latitude, longitude).ToString();
            data["gps.accuracy"] = accuracy;
        };
    }

    /// <summary>
    /// Creates a new provider based on provided parameters and also
    /// registers a callback to the external data source when updates occur.
    /// </summary>
    private void updateSettings(
        IConfigurationRoot configurationRoot,
        string latitudeStartKey,
        string longitudeStartKey,
        string latitudeEndKey,
        string longitudeEndKey)
    {
        configurationProvider = new GpsProgressConfigurationProvider(
            data,
            double.Parse(configurationRoot[latitudeStartKey]),
            double.Parse(configurationRoot[longitudeStartKey]),
            double.Parse(configurationRoot[latitudeEndKey]),
            double.Parse(configurationRoot[longitudeEndKey]));

        /**
         * If our data source changes, we want to create a new provider with the updated specs.
         * We need to register a callback each time the configuration changes since the callbacks
         * are lost once the token changes.
         */
        configurationRoot.GetReloadToken().RegisterChangeCallback((configuration) =>
        {
            updateSettings(
                configuration as IConfigurationRoot,
                latitudeStartKey,
                longitudeStartKey,
                latitudeEndKey,
                longitudeEndKey);
        }, configurationRoot);
    }

    private IDictionary<string, string> data = new Dictionary<string, string>();
    private GpsProgressConfigurationProvider configurationProvider;

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return configurationProvider;
    }
}

Creating the provider

The provider is responsible for doing the heavy lifting. It calculates and stores the distance between the start and end-points. It also calculates the progress by comparing a third-point against both of the stored points.

public class GpsProgressConfigurationProvider : ConfigurationProvider
{
    public GpsProgressConfigurationProvider(
        IDictionary<string, string> data,
        double latitudeStart,
        double longitudeStart,
        double latitudeEnd,
        double longitudeEnd)
    {
        Data = data;
        this.latitudeStart = latitudeStart;
        this.latitudeEnd = latitudeEnd;
        this.longitudeStart = longitudeStart;
        this.longitudeEnd = longitudeEnd;

        // Defines the length between start and end-points.
        absoluteDistance = getAngularDistanceBetweenPoints(latitudeStart, longitudeStart, latitudeEnd, longitudeEnd);
    }

    private readonly double absoluteDistance;
    private readonly double latitudeEnd;
    private readonly double latitudeStart;
    private readonly double longitudeEnd;
    private readonly double longitudeStart;

    /// <summary>
    /// Gets percent-progress as a decimal.
    /// </summary>
    public double GetProgress(double latitude, double longitude)
    {
        var distanceFromStart = getAngularDistanceBetweenPoints(latitudeStart, longitudeStart, latitude, longitude);
        var distanceFromEnd = getAngularDistanceBetweenPoints(latitudeEnd, longitudeEnd, latitude, longitude);

        /**
         * We are taking the average to make sure progress is relative to both
         * start and end-points. If we focus on just the end-point, then the "start"
         * becomes anywhere along the circumference of the imaginary circle where the center is
         * the end-point and the radius is the `absoluteDistance`.
         */
        var averageDistance = (distanceFromStart + (absoluteDistance - distanceFromEnd)) / 2;

        return averageDistance / absoluteDistance;
    }

    /// <summary>
    /// Gets the distance between two coords.
    /// Note: this uses the haversine formula instead of spherical law of cosines
    /// since the former does better over smaller distances.
    /// Also note: the larger the difference in latitude, the higher degree of inaccuracy.
    /// This is due to the formula's assumption of a spherical Earth.
    /// Ref. https://en.wikipedia.org/wiki/Haversine_formula
    /// Ref. http://www.movable-type.co.uk/scripts/latlong.html
    /// </summary>
    /// <returns>Non-unit of measure. To get unit-distance, multiply by Earth's mean radius (i.e. mi or km).</returns>
    private double getAngularDistanceBetweenPoints(
        double latitudeStart,
        double longitudeStart,
        double latitudeEnd,
        double longitudeEnd)
    {
        var latitudeDifference = toRadians(latitudeEnd - latitudeStart);
        var longitudeDifference = toRadians(longitudeEnd - longitudeStart);

        var haversine =
            Math.Pow(Math.Sin(latitudeDifference / 2), 2) +
            Math.Cos(toRadians(latitudeStart)) * Math.Cos(toRadians(latitudeEnd)) *
            Math.Pow(Math.Sin(longitudeDifference / 2), 2);

        var angularDistance = 2 * Math.Atan2(Math.Sqrt(haversine), Math.Sqrt(1 - haversine));

        return angularDistance;
    }

    private double toRadians(double degrees)
    {
        return degrees * (Math.PI / 180);
    }
}

Creating the extension

The helper extension is really just a bootstrapper to avoid having more code in the Startup .

public static class GpsProgressConfigurationExtensions
{
    public static IConfigurationBuilder AddGpsProgress(
        this IConfigurationBuilder configurationBuilder,
        IConfigurationRoot configurationRoot,
        string latitudeStartKey,
        string longitudeStartKey,
        string latitudeEndKey,
        string longitudeEndKey,
        out Action<double, double, string> positionChanged)
    {
        return configurationBuilder.Add(
            new GpsProgressConfigurationSource(
                configurationRoot,
                latitudeStartKey,
                longitudeStartKey,
                latitudeEndKey,
                longitudeEndKey,
                out positionChanged));
    }
}

Getting updates to the provider

A static property called DistancePositionChanged is provided which gets called by a controller-action as data is submitted by the browser client.

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var gpsConfiguration = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("gps.config.json")
            .AddJsonFile($"gps.config.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
            .Build();

        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddGpsProgress(
                gpsConfiguration,
                "latitudeStart",
                "longitudeStart",
                "latitudeEnd",
                "longitudeEnd",
                out DistancePositionChanged)
            .AddEnvironmentVariables();

        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    /// <summary>
    /// Latitude, longitude, accuracy.
    /// </summary>
    public static Action<double, double, string> DistancePositionChanged;
}

Controller

Here, we are just reading form-data sent from the browser-client and calling our static action defined in Startup (i.e. the same one managed by our config. provider).

public class GpsDistanceController : Controller
{
    [HttpPost]
    public async Task<IActionResult> Create()
    {
        var coordinates = await Request.ReadFormAsync();

        Startup.DistancePositionChanged(
            double.Parse(coordinates["latitude"]),
            double.Parse(coordinates["longitude"]),
            coordinates["accuracy"]);

        return new StatusCodeResult((int)HttpStatusCode.Accepted);
    }

    [HttpGet]
    public IActionResult Index()
    {
        return View();
    }
}

Browser logic

We are taping into the the browser's geolocation API to retrieve and submit the user's current location.

$(function() {
    if (navigator.geolocation) {
        navigator.geolocation.watchPosition(function(position) {
            var latitude = position.coords.latitude;
            var longitude = position.coords.longitude;
            var accuracy = position.coords.accuracy;

            $.post('/gpsdistance/create', {
                latitude: latitude,
                longitude: longitude,
                accuracy: accuracy
            });
        }, function(error) {
            showErrorMessage(error.message);
        }, { enableHighAccuracy: true });
    }
    else {
        showErrorMessage('Geolocation is not enabled on this device.');
    }

    function showErrorMessage(message) {
        alert(message);
    }
});

Controller to retrieve progress

Lastly, we have a simple controller for reporting progress.

public class HomeController : Controller
{
    public HomeController(IConfigurationRoot config)
    {
        this.config = config;
    }

    private IConfigurationRoot config;

    public IActionResult Index()
    {
        double progress;

        double.TryParse(config["gps.distance.progress"], out progress);

        ViewBag.Progress = (progress * 100).ToString("N0") ?? "0";
        ViewBag.Accuracy = config["gps.accuracy"] ?? "∞";

        return View();
    }
}

Result

Here is a demo I made and posted to Twitter . It demonstrates an updated progress bar based on on my travel between two pre-determined GPS points. I used markers on the grass so I knew where to start and end as well as the halfway-point. My phone is the client pushing location updates. I am also carrying a tablet which is displaying the same data as seen on the laptop in the foreground (the latter is not needed–I wanted to confirm things were working as-expected since I could not see the laptop screen).

Observations

  • Chrome no longer allows access to geolocation on non-secure sites (ref. https://developers.google.com/web/updates/2016/04/geolocation-on-secure-contexts-only?hl=en )
    • this was not a dealbreaker as I just used Firefox for Android which at the moment does not have this restriction.
  • geolocation is not consistent across mobile phones.
    • For my test purposes, Motorola Moto X (gen. 2) provided accuracy down to three-meters while Apple iPhone 5s only provided five-meters.
  • Longer distances provide better results
    • The demo. above had a distance of ~60 ft. Standing at the end-marker, this means my reported location could have been anywhere between ~51 and ~69 ft. At 51 ft., my progress would have been displayed as eighty-five percent. Taken to the extreme, if the overall distance was only ten-feet, then the given accuracy-factor could report progress as only ten-percent!




About List