Mobile Truck Tracker Web Site

I’ve been meaning to publish a bit on the mobile version of the Truck Tracker application. Previous posts have been created a geo-location service that tracks ‘trucks’ (the application can be used to track any asset as long as GPS data is being recorded). A huge component of this application is being able to upload geo-location data as it is being collected. Previously, I blogged about collecting GPS data using the Netduino and have provided a way to manually upload this data to the Truck Tracker application. Although possible, this is not a very user-friendly workflow. More recently, I posted about using geo-location enabled browsers to record trip data. This post will explore providing a front end for truck tracker that uses geo-location enabled browsers.

Mobile ASP.NET MVC Applications

One of the first tasks that must be done is to be able to detect if the browser is a mobile device. This information will allow the application to render HTML specific to the browser. This is important because the screen real-estate of a mobile device is much smaller than a typical desktop. The content rendered on the desktop generally does not provide adequate user experience on a mobile device. Here are screen shots from the desktop version (top) and mobile version (bottom) of the application home page:

image

imageAs you can see, the desktop and mobile version of the home page are quite different. They both use the same URL. So how is this done?

I am using the Mobile Browser Definition File and a custom ASP.NET MVC view engine. This approach was previously documented by Scott Hanselman. Adding the Mobile Browser Definition File quickly allows you to detect the mobile device type hitting your site. A number of variables are exposed that you can then use in your application. One of the features of the ASP.NET MVC framework is that it is highly customizable. In this case, we will be overriding the standard view engine to enable the view selection taking into account the information provided by the Mobile Browser Definition File. Here is the complete custom view engine (originally from Scott’s post, but repeated here for convenience).

public class MobileCapableWebFormViewEngine : WebFormViewEngine
{
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        ViewEngineResult result = null;
        var request = controllerContext.HttpContext.Request;

        // Avoid unnecessary checks if this device isn't suspected to be a mobile device
        if (request.Browser.IsMobileDevice)
        {
            result = base.FindView(controllerContext, "Mobile/" + viewName, masterName, useCache);
        }

        //Fall back to desktop view if no other view has been selected
        if (result == null || result.View == null)
        {
            result = base.FindView(controllerContext, viewName, masterName, useCache);
        }

        return result;
    }
}

As can be seen in the above code, if the browser is a mobile device the view is in a ‘Mobile’ sub-directory. Here is a screen shot of how the views are arranged in the application:

imageThis is how the application can use the same model / controller logic and provide a desktop or mobile view. The custom view engine is registered using the following code:

// Register mobile view engine
//
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MobileCapableWebFormViewEngine());

 

MVC Trip Recorder

The mobile application consists of creating mobile views for a number of existing desktop views. That work is not too exciting. It mostly involves updating the HTML / CSS to render a page that fits on a smaller screen and accounts for touch driven UI considerations (bigger buttons with plenty of padding…etc).

The second bit of work done to create the mobile application is to add a new controller that allows mobile devices to collect and post GPS data back to the server. This controller consists of the following code:

public class MobileController : BaseController
{
    private readonly TripSessions _tripSessions;

    public MobileController(ILogger logger,
                            DataService dataService,
                            TripSessions tripSessions)
        : base(logger, dataService)
    {
        _tripSessions = tripSessions;
    }

    public ActionResult Index()
    {
        User user = FetchUser();
        if(user==null)
        {
            return RedirectToAction("Index", "Home");
        }
        return RedirectToAction("Index", "Truck");
    }

    public ActionResult Trip(int truckId)
    {
        User user;
        Truck truck;
        if(!Authorize(truckId, out user, out truck))
        {
            if(user==null)
            {
                return RedirectToAction("Index", "Home");
            }
            return RedirectToAction("Index", "Truck");
        }
        return View(truck);
    }

    public string StartTrip(int truckId)
    {
        if(!Request.IsAjaxRequest())
        {
            return "";
        }
        User user;
        Truck truck;
        if (!Authorize(truckId, out user, out truck))
        {
            if (user == null)
            {
                return "";
            }
            return "";
        }
        TripSession tripSession = _tripSessions.CreateSession(user.UserName, truckId);
        if(tripSession!=null)
        {
            return tripSession.Id.ToString();
        }
        return "";
    }

    public string AddLocations(string id, JsonLocationPoint[] pts)
    {
        if(!Request.IsAjaxRequest())
        {
            return "";
        }
        Guid tripId;
        try
        {
            tripId = new Guid(id);
        }
        catch (Exception)
        {
            _logger.Error("Invalid trip id. id=" + id);
            return "invalid trip id";
        }

        // Get the trip session
        //
        TripSession tripSession = _tripSessions.GetSession(tripId);
        if(tripSession==null)
        {
            _logger.Error("Failed to find trip session.");
            return "failed to find trip session";
        }

        // Get the truck and the trip
        //
        User user = _dataService.Users.FindBy(x => x.UserName == tripSession.UserName);
        if(user==null)
        {
            _logger.Error("User not found. username=" + tripSession.UserName);
            return "user not found";
        }
        Truck truck = _dataService.Trucks.FindBy(tripSession.TruckId);
        if(truck==null)
        {
            _logger.Error("Truck not found. truckid=" + tripSession.TruckId);
            return "truck not found";
        }

        string result = "";
        foreach (JsonLocationPoint pt in pts)
        {
            Location location = new Location
            {
                Latitude = pt.lat,
                Longitude = pt.lng,
                Timestamp = pt.ts,
                Truck = truck
            };
            IEnumerable<string> brokenRules;
            if (!_dataService.Locations.Add(location, out brokenRules))
            {
                result += brokenRules.First() + ",";
            }
        }
        _dataService.Commit();
        if(result=="")
        {
            return "Ok";
        }
        return result;
    }

}

 

Recall that I am using Ninject for my IOC container. The constructor takes three objects. The ‘DataService’ is the wrapper around the data access / repositories that we previously created. The ‘TripSessions’ object provides session management services where the session data will exist across application recycles. This is important for allowing the recording of trips that last longer than a typical internet session.

The ‘Index’ action method is simply a redirector just in case you hit this URL by accident. The ‘Trip’ action method first verifies the user is authorized to record trip data for the given truck. It then renders a view that allows trip recording to begin. This view is essentially the view created in the previous trip recorder and will not be repeated here.

Two enhancements to the JavaScript in the old trip recorder have been made. The first enhancement is the retrieval of a ‘token’ from the server when a trip is started. The client side part of this request is handled using the following JavaScript:

$("#toggle").click(function (evt) {
    evt.preventDefault();
    if (!isStarted) {
        $(this).html("End Trip").removeClass("green-btn").addClass("red-btn");
        var truckId = $("#truckId").val();
        var url = '<%= Url.Action("StartTrip", "Mobile") %>';
        $.get(url, { truckId: truckId }, function (data) {
            if (data != "") {
                tripId = data;
                startGps();
                isStarted = true;
            } else {
                alert("Could not contact the server.");
                $("#toggle").html("Start Trip").removeClass("red-btn").addClass("green-btn");
            }
        });
    } else {
        $(this).html("Start Trip").removeClass("red-btn").addClass("green-btn");
        stopGps();
        isStarted = false;
    }
});

A call to the ‘StartTrip’ action method (code shown previously) creates a new ‘TripSession’ that is stored in the longer term session management service. A ‘token’ (a GUID) is then returned to the client. Once the ‘token’ has been acquired, the main loop that collects the GPS points is started. This ‘token’ is then sent with with GPS data to the server to indicate which user / truck the points belong.

The second enhancement to the JavaScript adds the ability to send the accumulated GPS points back to the server. The previous trip recorder accumulated the GPS points into a JavaScript array and displayed them in the browser. The enhanced version does this, but also starts a JavaScript interval timer (currently set to every 10 seconds) that posts all new points to the server. The bunching of points on the client prevents the clients from becoming ‘too chatty’ and allows for more efficient database calls. Here is the JavaScript used to post the points:

var indexOfLastSent = 0;
function sendPts() {
    // Batch the points
    var count = pts.length;
    var numToSend = count - indexOfLastSent;
    if (numToSend <= 0) {
        return;
    }

    // Package up the data
    var data = new Object();
    data.id = tripId;

    var ptsToSend = [];
    for (var i = indexOfLastSent; i < count; i++) {
        var pt = new Object();
        pt.lat = pts[i].coords.latitude;
        pt.lng = pts[i].coords.longitude;
        pt.ts = new Date(pts[i].timestamp);
        ptsToSend.push(pt);
    }
    indexOfLastSent = count;
    data.pts = ptsToSend;

    var numPoints = ptsToSend.length;
    var url = '<%= Url.Action("AddLocations", "Mobile") %>';
    $.post(
            url, $.postify(data),
            function (data) {
                if (data == 'Ok') {
                    ptsSent += numPoints;
                    updateStatus();
                } else {
                    console.log("post returned: " + data);
                }
            });
}

The ‘indexOfLastSent’ is the marker into the GPS data array indicating what has been sent. Once we determine there is new GPS data to send, the data are bundled together into a JavaScript array, and posted (using jQuery ‘post’) to the server. Notice the ‘token’ we previously got from the server is sent in the bundle with the data. The ‘AddLocations’ action method (see code above) then validates the ‘token’, grabs the trip session data, and then commits the new GPS points to the database.

Summary

Now if you have a geo-location enabled browser on your mobile device you can record trip data for your vehicle. I encourage you to register a vehicle and give the system a try. Let me know if you find any issues.

Comments
  1. Kyle Banashek
    • rcravens

Leave a Reply to rcravens Cancel reply

Your email address will not be published. Required fields are marked *

*