An ASP.NET MVC Truck Tracker Application Using Google Maps

In a previous post we built a MySQL database and a data access layer via a repository pattern implemented using Fluent LINQT to NHibernate. We also wired up this repository in our ASP.NET MVC application using Ninject. This post will build out the ASP.NET MVC application a bit to present the information. To help visualize the data we will be using the Google Maps v.3 jQuery plugin that we previously built.

MVC View

Here is a screen shot of the view we are going to build.

image There is a header and footer on the page. The header contains application title and subtitle designed to look like a road. The footer contains information about the technology with links to the information.

The core of the application is a master / slave list and map view. Here is the markup that creates this view.

<head runat="server">
    <title>Trucks</title>
    <link type="text/css" href="~/Content/css/Site.css" rel="Stylesheet" />

    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
    <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
    <script type="text/javascript" src="<%= Url.Content("~/Content/scripts/jquery.gmap3.js") %>"></script>
    <script type="text/javascript" src="<%= Url.Content("~/Content/scripts/trucktracker.js") %>"></script>
</head>
<body>

    <div class="header">
        <!-- Conent removed for brevity -->
    </div>

    <div id="wrapper">

        <div id="truck-list" class="left">
            <h3>Truck List</h3>
            <ul>
                <% foreach(Truck truck in Model) { %>
                <li>
                    <a id="truck-<%= truck.Id %>" href="javascript:showTruck(<%= truck.Id %>)">
                        <span class="truck">
                            <span class="truck-id"><%= truck.Id %></span>
                            <span class="truck-name"><%= truck.Name %></span>
                            <span class="truck-type"><%= truck.Type %></span>
                            <span class="truck-plate"><%= truck.PlateNumber %></span>
                        </span>
                    </a>
                </li>
                <% } %>
            </ul>
        </div>

        <div id="map">
            <div id="map_canvas" class="line"></div>
        </div>

        <div id="footer">
            <!-- Content removed for brevity -->
         </div>
    </div>
</body>

The above code has been simplified by removing elements that do not map to functional requirements (header and footer areas) of the list view / map details. In the HTML ‘head’ element all the necessary CSS and scripts are pulled down. Notice the inclusion of jQuery and the Google maps API (both from the Google repository).  The last two scripts include the previously created Google maps jQuery plugin and the application specific JavaScript (more later).

The bulk of the markup defines the layout for the list view (‘truck-list’ division). This division builds up the truck list using the supplied view-model (a list of ‘Truck’ objects). Each list element is an HTML anchor “obtrusively” wired up to the ‘showTruck’ JavaScript handler (more in a bit). The remaining elements create content and define CSS classes to allow each element to be styled.

The map markup is an empty HTML ‘div’. This map is dynamically created in JavaScript as the page loads. The ‘showTruck’ JavaScript handler dynamically updates the map area with content specific to a selected truck. Let’s look at the JavaScript.

JavaScript

This page leverages a bit of JavaScript to dynamically generate HTML. Lucky for us jQuery hides a lot of complexity that is required to keep the page cross-browser compliant. Here is the JavaScript for the list view / map area:

// Global map variable
var map;

// Main entry point...runs on document ready
$(document).ready(function () {
    // Create and load the initial map
    map = $("#map_canvas").gmap3(
            {
                lat: 43.0566,
                lng: -89.4511,
                zoom: 2
            });
    map.setTypeRoadMap();
});

// AJAX fetch the last 50 GPS points
function getHistory(id) {
    var url = 'Home/History';
    $.getJSON(url, { truckId: id, count: 50 }, function (data) {
        map.addPath(data);
    });
}

// AJAX fetch the truck info
function getLocation(id) {
    var url = 'Home/Position';
    $.getJSON(url, { truckId: id, count: 50 }, function (data) {
        map.setCenter(data.lat, data.lng);
        map.setZoom(6);
        // we have the lat/lng...geocode to get address
        $.fn.gmap3.geoCodeLatLng(data.lat, data.lng, function (address) {
            var latlng = data.lat + ", " + data.lng;
            map.addMarkerByLatLng(data.lat, data.lng, data.name, createInfo(data.name, data.plate, data.type, data.driver, latlng, address));
        });

    });
}

// Helper to create the map info bubble
function createInfo(title, plate, type, driver, latlng, location) {
    return '<div class="popup"><h1 class="popup-title">' + title + '</h1><div id="popup-body"><p><span class="popup-label">plate:</span> ' + plate + '</p><p><span class="popup-label">type:</span> ' + type + '</p><p><span class="popup-label">driver:</span> ' + driver + '</p><p><span class="popup-label">lat/lng:</span> ' + latlng + '</p><p><span class="popup-label">location:</span> ' + location + '</p></div></div>';
}

// List object click handler
function showTruck(id) {
    map.clear();
    getLocation(id);
    getHistory(id);
    $(".selected").removeClass("selected");
    $("#truck-" + id).addClass("selected");
}

The jQuery document ready event creates the initial map by using the before mentioned Google maps jQuery plugin. The map initial center, zoom, and type are set. Next are two JavaScript functions ‘getHistory’ and ‘getLocation’ that asynchronously (AJAX) fetch data from the server.

The ‘getHistory’ function fetches the last 50 lat / long GPS points for a truck. This information is returned in a JavaScript array of JSON formatted data. This information is in the format required by the map plugin to add a path. The ‘getHistory’ function uses the ‘History’ action method (discussed below) on the ‘Home’ controller.

The ‘getLocation’ function fetches detailed information about the truck. This information is used to reset the map center and zoom level. The detailed information includes the latest GPS lat / long point of the truck which is then geo-coded using Google’s map API to get an address. This information is then used to add a marker to the map via a function on the jQuery map plugin. This marker includes a popup that is dynamically created in the ‘createInfo’ function. The ‘getLocation’ function uses the ‘Position’ action method (discussed below) on the ‘Home’ controller.

The ‘showTruck’ function is the JavaScript event handler for the HTML anchors in the truck list view. When a truck is clicked in the list the map is cleared, location / history data is fetched and the CSS ‘selected’ class is managed. The CSS ‘selected’ class allows the selected element to be visually high lighted.

MVC Controller

The controller for the above view is defined by the following code:

public class HomeController : Controller
{
    private readonly IIntKeyedRepository<Truck> _truckRepository;

    public HomeController(IIntKeyedRepository<Truck> truckRepository)
    {
        _truckRepository = truckRepository;
    }

    public ActionResult Index()
    {
        return View(_truckRepository.All());
    }

    public ActionResult Position(int truckId)
    {
        JsonMapper mapper = new JsonMapper(_truckRepository);
        JsonTruckInfo jsonTruckInfo = mapper.MapToTruckInfo(truckId);
        if( jsonTruckInfo != null )
        {
            return Json(jsonTruckInfo, JsonRequestBehavior.AllowGet);
        }
        return new EmptyResult();
    }

    public ActionResult History(int truckId, int count)
    {
        JsonMapper mapper = new JsonMapper(_truckRepository);
        List<JsonLocation> locations = mapper.MapToLocations(truckId, count);
        if (locations != null)
        {
            return Json(locations, JsonRequestBehavior.AllowGet);
        }
        return new EmptyResult();
    }
}

As previously discussed, we are using Ninject as a Inversion of Control (IoC) container. In order to handle object lifetime, Ninject provides a controller factory for our ASP.NET MVC application. This controller factory does not require a default (parameter-less) constructor. The ‘HomeController’ constructor above accepts an ‘IIntKeyedRepository<Truck>’ instance. This repository instance is created by Ninject and provides access to the database.

The ‘Index’ action method simple provides an enumerable list of ‘Truck’ objects to the view. The ‘Position’ and ‘History’ action methods each use a mapper object to generate a view-model. The view-model objects are then serialized and returned in JSON format to the browser.

MVC Models

The following code defines the mapper class that is used by the controller to generate view-model objects:

public class JsonMapper
{
    private readonly IIntKeyedRepository<Truck> _truckRepository;

    public JsonMapper(IIntKeyedRepository<Truck> truckRepository)
    {
        _truckRepository = truckRepository;
    }

    public JsonTruckInfo MapToTruckInfo(int truckId)
    {
        Truck truck = _truckRepository.FindBy(truckId);
        if (truck != null)
        {
            Location location = truck.Locations.OrderByDescending(c => c.Timestamp).First();
            JsonTruckInfo jsonTruckInfo = new JsonTruckInfo
            {
                driver = truck.Driver.FirstName + " " + truck.Driver.LastName,
                lat = location.Latitude,
                lng = location.Longitude,
                name = truck.Name,
                plate = truck.PlateNumber,
                type = truck.Type
            };
            return jsonTruckInfo;
        }
        return null;
    }

    public List<JsonLocation> MapToLocations(int truckId, int count)
    {
        Truck truck = _truckRepository.FindBy(truckId);
        if (truck != null)
        {
            List<JsonLocation> locations =
                truck.Locations.OrderByDescending(c => c.Timestamp).Take(count).Select(
                    location => new JsonLocation { lat = location.Latitude, lng = location.Longitude }).ToList();

            return locations;
        }
        return null;
    }
}

An instance of the mapper object uses the injected repository to fetch data from the database and map that data into the one of the following view-models:

public class JsonTruckInfo
{
    public double lat { get; set; }
    public double lng { get; set; }
    public string name { get; set; }
    public string type { get; set; }
    public string plate { get; set; }
    public string driver { get; set; }
}

public class JsonLocation
{
    public double lat { get; set; }
    public double lng { get; set; }
}

It should be noted that the ‘MapToLocations’ method returns a list of ‘JsonLocation’ objects which when serialized to JSON have the exact format required by the jQuery map plugin.

Summary

Adding a presentation layer to the truck tracker data was relatively easy. Especially since we previously created the database, repository, jQuery mapping plugin, and wired up these objects with our IOC container.

I am in the process of deploying this application to the internet. I will post again once that is deployed and will include a download of the solution at that time.

What’s next? We now need a way to get GPS location data into the database. I will be creating a web service that provides this feature. Check back as we explore the various web service options (ASMX, MVC, WCF…etc) to get data into our database.

Comments
  1. Scott
  2. Bob Cravens
  3. leoenel
  4. Tom
    • Tom
      • rcravens
  5. Ashwani
    • matias
  6. V.Maheedhar
    • rcravens
  7. Luís Ferreira
  8. Jeff
  9. Vian
  10. Sundeep
    • rcravens
  11. Mathews
  12. Sachin
  13. amin
  14. Kyle Banashek

Leave a Reply

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

*