GPS Nerd Is Published to CodePlex

I have been meaning to do this for quite some time. Many of you have been asking me for the code for the GPS Nerd (a.k.a. Truck Tracker) project. My “just one more thing” attitude has delayed this quite a bit. From the CodePlex site you can download the all the code. In addition, I tried to compile links to the various blog posts on the CodePlex site.

image

Code

The concept of the project is to create an ASP.NET MVC application leveraging a number of technologies (MySQL, NHibernate, Ninject, Google Maps API…etc) and bring these together using a number of ‘best practices’. I have learned a lot. I hope that it will serve as a positive example of code for the community. The project will continue to evolve, so consider this ‘drop number one’. I have enjoyed creating this application and working with these technologies.

If you want to contribute, the project is now open source. This means you can download and improve the project. If you want to collaborate or have questions, please let me know. If you have some “designer skills” and want to help make the site more appealing, please join the project. There are a number of ideas that I still have and want to pursue. However, this is an opportunity for the project to become more than just my “sandbox that I play in on the weekends”.

Posted in Uncategorized | 1 Comment

A Windows Phone 7 Trip Recorder

Previously I developed a trip recorder using HTML, CSS, and JavaScript leveraging JavaScript’s geolocation API. I recently switched from an iPhone to Windows Phone 7 (WP7) and noticed that mobile Internet Explorer doesn’t support the geolocation API. After a bit of a rant about IE not supporting something so fundamental to a mobile device, I quickly moved forward to see an opportunity to develop a trip recorder application for WP7. This is a long post, but covers all of the steps to create this application. Here a few screenshot (from the emulator) of the application:

data_not_started map  settings

 

Getting Started

There are a few pre-requisites that will help get you started for this project. These have been covered in numerous places and I will only provide links.

  • Getting Started with Windows Phone 7 Development – A nice summary of Windows Phone 7 and how to get started by Dan Wahlin.
  • 31 Days of Windows Phone 7 – In this ‘how to’ series of posts, Jeff Blankenburg breaks down a number of Windows Phone 7 topics.
  • Microsoft’s App Hub – This site helps you get registered as a WP7 developer, download the development tools and manage your account (e.g. publish apps). Once you are registered there is an approval process before you can deploy any apps to your WP7 hardware. In the meantime, Microsoft supplies an emulator for you to use.

You should know that this emulator (from what I have read) is a virtual machine and may have issues running inside another virtual machine. I am developing using the latest version (6) of Parallels for the Mac and have not had any issues. A friend had version 5 and it would not work.

  • Bing Maps Portal – This site helps you get registered as a Bing Maps developer. Once you are registered you will receive a ‘key’ to remove the ‘unregistered user’ message from the Bing Maps Silverlight control. You also gain access to the Bing Maps web services.
  • MVVM Explained – A nice explanation of the Model-View-ViewModel pattern by Jeremy Likness.

Bing Maps Reverse Geocoding Service

Many times you want to be able to translate GPS latitude / longitude coordinates into a street address. This process is known as reverse geocoding. Bing provides a “Spatial Data Services” service to do this. The service publishes a WSDL and it seemed like it would be straight-forward to a “Service Reference” in Visual Studio. Usually this reads the WSDL and generates a proxy for you to use in your application. Unfortunately, this failed every time I tried. No problem. There is a command line tool for Silverlight that generates the proxy classes for you. Here are the command line command and response from generating the proxy classes:

Microsoft Windows [Version 6.1.7600]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Windows\system32>cd "c:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Tools"

c:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Tools>SlSvcUtil.exe http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc?wsdl
Attempting to download metadata from 'http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc?wsdl' using WS-Metadata Exchange or DISCO.
Generating files...

c:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Tools\GeocodeService.cs

c:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Tools\ServiceReferences.ClientConfig

In the above snippet, you can see the path of the tool (I am on a 64 bit system so it may just be ‘c:\Program Files\’). To run the proxy generator service utility, just feed the URL to the WSDL service as the command line parameter. The service utility generated two files: ‘GeocodeService.cs’ and ‘ServiceReferences.ClientConfig’. Add these two you application (the latter file needs to be in the application root). Then you will be able to use the service with the following code:

private void ReverseGeocode(double lat, double lng)
{
    // Reverse geocode using the Bing service.
    //
    ReverseGeocodeRequest reverseGeocodeRequest = new ReverseGeocodeRequest
                                                      {
                                                          Credentials = new Credentials
                                                                            {
                                                                                ApplicationId = Keys.BingMaps
                                                                            }
                                                      };

    Location point = new Location { Latitude = lat, Longitude = lng };
    reverseGeocodeRequest.Location = point;

    try
    {
        GeocodeServiceClient geocodeServiceClient = new GeocodeServiceClient("BasicHttpBinding_IGeocodeService");
        geocodeServiceClient.ReverseGeocodeCompleted += GeocodeServiceClientReverseGeocodeCompleted;
        geocodeServiceClient.ReverseGeocodeAsync(reverseGeocodeRequest);
    }
    catch (Exception ex)
    {
        // Could be the revsese geocode service is off-line
        ReverseGeocodeCompletedEventArgs fake = new ReverseGeocodeCompletedEventArgs(null, ex, false, null);
        GeocodeServiceClientReverseGeocodeCompleted(this, fake);
    }
}

private void GeocodeServiceClientReverseGeocodeCompleted(object sender, ReverseGeocodeCompletedEventArgs e)
{
    // Asnyc callback for completed Bing reverse geocode.
    //
    GeocodeResponse geocodeResponse = e.Result;
    Address = geocodeResponse.Results.Length > 0 ? geocodeResponse.Results[0].DisplayName : "not found";
}

The call to the service is asynchronous. The first method accepts the lat/long GPS coordinates and make the reverse geocoding call to the service using the proxy classes we just built. Note that the ‘ReverseGeocodeRequest’ object has a ‘Credentials’ property that is initialized with the ‘ApplicationId’ I received when I registered as a Bing Maps developer. The second method is the callback that provides the address data. There are times where the initial call to the service may throw exceptions (e.g. end point not available). In those instances, the exception is caught and the callback method invoked with an empty result leading to the ‘not found’ case.

WP7 Infrastructure

The WP7 hardware allows your application to store settings and files. This is done using the ‘System.IO.IsolatedStorage’ namespace. Directly using this namespace can lead to code that cannot be tested without the hardware (or emulator) available. To facilitate automated testing, this infrastructure is encapsulated by the following interfaces and implementations.

public interface IAppSettings
{
    void Save(string name, object value);
    object Load(string name);
    void Remove(string name);
}

public class AppSettings : IAppSettings
{
    public void Save(string name, object value)
    {
        IsolatedStorageSettings.ApplicationSettings[name] = value;
    }

    public object Load(string name)
    {
        if(IsolatedStorageSettings.ApplicationSettings.Contains(name))
        {
            return IsolatedStorageSettings.ApplicationSettings[name];
        }
        return null;
    }

    public void Remove(string name)
    {
        if(IsolatedStorageSettings.ApplicationSettings.Contains(name))
        {
            IsolatedStorageSettings.ApplicationSettings.Remove(name);
        }
    }
}

The above code encapsulates the storing of application session data (name/value pairs). The following encapsulate the file system:

public interface IFileSystem
{
    void CreateDirectory(string path);
    bool DirectoryExists(string path);
    string GetDirectoryName(string path);

    bool FileExists(string path);
    void WriteAllText(string path, string contents);
    void WriteAllLines(string path, string[] contents);
    string ReadAllText(string path);
    string[] ReadAllLines(string path);
}

public class Wp7FileSystem : IFileSystem
{
    private readonly IsolatedStorageFile _isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication();

    public void CreateDirectory(string path)
    {
        _isolatedStorageFile.CreateDirectory(path);
    }

    public bool DirectoryExists(string path)
    {
        return _isolatedStorageFile.DirectoryExists(path);
    }

    public string GetDirectoryName(string path)
    {
        return Path.GetDirectoryName(path);
    }

    public bool FileExists(string path)
    {
        return _isolatedStorageFile.FileExists(path);
    }

    public void WriteAllText(string path, string contents)
    {
        StreamWriter writer =
            new StreamWriter(new IsolatedStorageFileStream(path, FileMode.OpenOrCreate,
                                                           _isolatedStorageFile));

        writer.Write(contents);
        writer.Close();
    }

    public void WriteAllLines(string path, string[] contents)
    {
        string data = string.Join("\n", contents);
        WriteAllText(path, data);
    }

    public string ReadAllText(string path)
    {
        StreamReader reader = null;
        string data = null;
        if (FileExists(path))
        {
            try
            {
                reader = new StreamReader(new IsolatedStorageFileStream(path, FileMode.Open, _isolatedStorageFile));
                data = reader.ReadToEnd();
            }
            catch (Exception)
            {

            }
            finally
            {
                if (reader != null)
                {
                    reader.Close();
                }
            }
        }
        return data;
    }

    public string[] ReadAllLines(string path)
    {
        string data = ReadAllText(path);
        if(!string.IsNullOrEmpty(data))
        {
            return data.Split('\n');
        }
        return null;
    }
}

The ‘IFileSystem’ interface contains the features that are needed for this application. It could easily expand to contain others. These two interfaces can be faked or mocked to allow code that depends on the WP7 file system to be tested without the need for the WP7 hardware or emulator.

 

Repository

The persistence mechanism in this application will consist of saving collected GPS data to a file using a simple format. The entities are defined by the following class:

public class TimeStampedLocation
{
    public DateTime Timestamp { get; set; }
    public GeoCoordinate Location { get; set; }

    public override string ToString()
    {
        return Timestamp.ToString("o") + ","
               + Location.Latitude + ","
               + Location.Longitude + ","
               + Location.Altitude + ","
               + Location.HorizontalAccuracy + ","
               + Location.VerticalAccuracy + ","
               + Location.Course + ","
               + Location.Speed;

    }

    public static TimeStampedLocation Parse(string value)
    {
        string[] parts = value.Split(',');
        if (parts.Length != 8)         {
            return null;
        }

        DateTime timeStamp;
        if(!DateTime.TryParse(parts[0], out timeStamp))
        {
            return null;
        }

        double latitude;
        if(!double.TryParse(parts[1], out latitude))
        {
            return null;
        }

        double longitude;
        if(!double.TryParse(parts[2], out longitude))
        {
            return null;
        }

        double altitude;
        if(!double.TryParse(parts[3], out altitude))
        {
            return null;
        }

        double horzAccuracy;
        if (!double.TryParse(parts[4], out horzAccuracy))
        {
            return null;
        }

        double vertAccuracy;
        if (!double.TryParse(parts[5], out vertAccuracy))
        {
            return null;
        }

        double course;
        if (!double.TryParse(parts[6], out course))
        {
            return null;
        }

        double speed;
        if (!double.TryParse(parts[7], out speed))
        {
            return null;
        }
        return new TimeStampedLocation
                   {
                       Timestamp = timeStamp,
                       Location = new GeoCoordinate(latitude, longitude, altitude, horzAccuracy, vertAccuracy, speed, course)
                   };
    }
}

This entity has two properties: ‘Timestamp’ and ‘Location’. The ‘Location’ property is a ‘System.Device.Location.GeoCoordinate’ object which encapsulates all the GPS data (lat, long, altitude, accuracy…). The two methods provide features for serialization / de-serialization.

The following interface defines the repository. This interface allows points to be added, the list to be cleared and the data persistence.

public interface ILocationRepository
{
    List<TimeStampedLocation> Locations { get; }

    int Count { get; }
    double DistanceInMiles { get; }
    double DistanceInKilometers { get; }

    void Add(TimeStampedLocation location);
    void ClearAll();

    void Save();
    void Load();
}

The two distance properties provide the total distance represented by all the points in the collection. Distance calculation is done using the Haversine Formula which is implemented as follows:

public static class GeoDistanceCalculator
{
    private const double _earthRadiusInMiles = 3956.0;
    private const double _earthRadiusInKilometers = 6367.0;
    public static double DistanceInMiles(double lat1, double lng1, double lat2, double lng2)
    {
        return Distance(lat1, lng1, lat2, lng2, _earthRadiusInMiles);
    }

    public static double DistanceInKilometers(double lat1, double lng1, double lat2, double lng2)
    {
        return Distance(lat1, lng1, lat2, lng2, _earthRadiusInKilometers);
    }

    private static double Distance(double lat1, double lng1, double lat2, double lng2, double radius)
    {
        // Implements the Haversine formulat http://en.wikipedia.org/wiki/Haversine_formula
        //
        var lat = ToRadians(lat2 - lat1);
        var lng = ToRadians(lng2 - lng1);
        var sinLat = Math.Sin(0.5 * lat);
        var sinLng = Math.Sin(0.5 * lng);
        var cosLat1 = Math.Cos(ToRadians(lat1));
        var cosLat2 = Math.Cos(ToRadians(lat2));
        var h1 = sinLat * sinLat + cosLat1 * cosLat2 * sinLng * sinLng;
        var h2 = Math.Sqrt(h1);
        var h3 = 2 * Math.Asin(Math.Min(1, h2));
        return radius * h3;
    }

    private static double ToRadians(double degrees)
    {
        return 2.0*Math.PI*degrees/360.0;
    }
}

The implementation of the repository interface is shown below.

public class LocationRepository : ILocationRepository
{
    private readonly List<TimeStampedLocation> _locations = new List<TimeStampedLocation>();
    private const string _relativePath = "data\\trip.txt";

    private readonly IFileSystem _fileSystem;

    public LocationRepository(IFileSystem fileSystem)
    {
        _fileSystem = fileSystem;
    }

    public int Count { get { return _locations.Count; } }

    public List<TimeStampedLocation> Locations { get { return _locations; } }

    public double DistanceInMiles
    {
        get { return CalculateDistance(false); }
    }

    public double DistanceInKilometers
    {
        get { return CalculateDistance(true); }
    }

    public void Add(TimeStampedLocation location)
    {
        lock(_locations)
        {
             _locations.Add(location);
        }
    }

    public void ClearAll()
    {
        lock (_locations)
        {
            _locations.Clear();
        }
    }

    public void Load()
    {
        lock (_locations)
        {
            // Clear the current locations
            //
            ClearAll();

            string data = _fileSystem.ReadAllText(_relativePath);
            if (data != null)
            {
                // Parse the data
                //
                string[] parts = data.Split('\t');
                foreach (string part in parts)
                {
                    TimeStampedLocation location = TimeStampedLocation.Parse(part);
                    if(location!=null)
                    {
                        Add(location);
                    }
                }
            }
        }
    }

    public void Save()
    {
        lock (_locations)
        {
            StringBuilder data = new StringBuilder();
            foreach (TimeStampedLocation location in _locations)
            {
                data.Append(location + "\t");
            }
            if (!_fileSystem.FileExists(_relativePath))
            {
                // Create the directory
                string relativeDir = _fileSystem.GetDirectoryName(_relativePath);
                _fileSystem.CreateDirectory(relativeDir);
            }
            _fileSystem.WriteAllText(_relativePath, data.ToString());
        }
    }

    private double CalculateDistance(bool isMetric)
    {
        double totalDistance = 0.0;
        for (int i = 1; i < _locations.Count; i++)
        {
            GeoCoordinate pos = _locations[i].Location;
            GeoCoordinate last = _locations[i - 1].Location;
            double distanceFromLastPoint = isMetric
                                               ? GeoDistanceCalculator.DistanceInKilometers(pos.Latitude,
                                                                                            pos.Longitude,
                                                                                            last.Latitude,
                                                                                            last.Longitude)
                                               : GeoDistanceCalculator.DistanceInMiles(pos.Latitude,
                                                                                       pos.Longitude,
                                                                                       last.Latitude,
                                                                                       last.Longitude);

            totalDistance += distanceFromLastPoint;
        }
        return totalDistance;
    }
}

 

The constructor takes an ‘IFileSystem’ implementation as a parameter. This is used by the ‘Save’ and ‘Load’ methods to serialize and de-serialize the data to the file system. The only other bit of complex code is the ‘CalculateDistance’ method which loops through all the points accumulating the distances calculated using the Haversine implementation.

GPS Service

The ‘System.Device.Location’ namespace provides access to the WP7 GPS hardware. The main access is through the ‘GeoCoordinateWatcher’ class. Once again, to isolate our code from the hardware implementation and enable features to be faked/mocked for testing purposes the GPS service used by this application is designed to an interface. In this case, the ‘GeoCoordinateWatcher’ class implements the ‘IGeoPositionWatcher<GeoCoordinate>’ interface. The following code defines the GPS watcher by extending this interface.

public interface IGpsService : IGeoPositionWatcher<GeoCoordinate>
{
    event EventHandler<TimeStampedLocationEventArgs> PointAdded;
    bool IsReady { get; }
    double TotalDistanceInMeters { get; }
    int Count { get; }
    List<TimeStampedLocation> Locations { get; }
    void Clear();
    void Save();
    void Load();
}

public class TimeStampedLocationEventArgs : EventArgs
{
    public TimeStampedLocation Point { get; set; }
}

The ‘IGpsService’ interface inherits ‘IGeoPositionWatcher<GeoCoordinate>’ and as such exposes the features to control and receive data from the GPS. Beyond the ability to start/stop, the ‘IGeoPositionWatcher<GeoCoordinate>’ implementation exposes two events. One provides status (e.g. initializing, ready) information about the GPS hardware. The other provides access to new GPS points as they are calculated by the GPS engine.

The GPS data coming out of the WP7 will have varying accuracy. Inside a building the hardware may use cell phone tower triangulation and provide a low accuracy point. Outdoors the accuracy will improve based upon the number of satellites the hardware ‘sees’. For this application, we want to observe all the GPS data points, but only add accurate data to the repository. The ‘PointAdded’ event is raised when an accurate point is added to the repository.

The ‘TotalDistanceInMeters’, ‘Locations’, ‘Clear’, ‘Save’, and ‘Load’ features allow the users of this object to interact with the repository. These are a light-weight wrapper around the equivalent ‘IRepository’ features without exposing the full ‘IRepository’ interface. For example, the users of the ‘IGpsService’ can save/load data (from the repository) but they cannot add new points.

The implementation of the ‘IGpsService’ is a bit long and probably deserves some refactoring. Here is the current version:

public class GpsService : IGpsService
{
    public event EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>> PositionChanged;
    public event EventHandler<GeoPositionStatusChangedEventArgs> StatusChanged;
    public event EventHandler<TimeStampedLocationEventArgs> PointAdded;

    private readonly IGeoPositionWatcher<GeoCoordinate> _watcher;
    private readonly ILocationRepository _locationRepo;

    public bool IsReady { get; set; }
    public double TotalDistanceInMeters { get; set; }
    public int Count { get { return _locationRepo.Count; } }
    public List<TimeStampedLocation> Locations { get { return _locationRepo.Locations; } }

    public GpsService(IGeoPositionWatcher<GeoCoordinate> watcher, ILocationRepository locationRepository)
    {
        _watcher = watcher;
        _locationRepo = locationRepository;

        TotalDistanceInMeters = _locationRepo.DistanceInKilometers*1000.0;

        _watcher.PositionChanged += WatcherPositionChanged;
        _watcher.StatusChanged += WatcherStatusChanged;
    }

    // Expose the gps watcher controls
    //
    public GeoPosition<GeoCoordinate> Position
    {
        get { return _watcher.Position; }
    }
    public GeoPositionStatus Status
    {
        get { return _watcher.Status; }
    }
    public void Start()
    {
        _watcher.Start();
    }
    public void Start(bool suppressPermissionPrompt)
    {
        _watcher.Start(suppressPermissionPrompt);
    }
    public bool TryStart(bool suppressPermissionPrompt, TimeSpan timeout)
    {
        return _watcher.TryStart(suppressPermissionPrompt, timeout);
    }
    public void Stop()
    {
        _watcher.Stop();
    }

    // Expose the location repo controls
    //
    public void Clear()
    {
        _locationRepo.ClearAll();
    }
    public void Save()
    {
        _locationRepo.Save();
    }
    public void Load()
    {
        _locationRepo.Load();
    }

    private void WatcherStatusChanged(object sender, GeoPositionStatusChangedEventArgs e)
    {
        IsReady = e.Status == GeoPositionStatus.Ready;
        if (StatusChanged != null)
        {
            StatusChanged(sender, e);
        }
    }

    private void WatcherPositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)
    {
        if (e.Position.Location.HorizontalAccuracy <= 20)
        {
            TimeStampedLocation point = new TimeStampedLocation
            {
                Timestamp = e.Position.Timestamp.Date,
                Location = e.Position.Location
            };
            if (_locationRepo.Count > 0)
            {
                GeoCoordinate last = _locationRepo.Locations[_locationRepo.Count - 1].Location;
                double distanceFromLastPointInMiles =
                    GeoDistanceCalculator.DistanceInMiles(e.Position.Location.Latitude,
                                                          e.Position.Location.Longitude,
                                                          last.Latitude, last.Longitude);

                // Ignore the points that are closer than the minimum distance.
                //
                if (distanceFromLastPointInMiles > 0.005)
                {
                    TotalDistanceInMeters += distanceFromLastPointInMiles * 1609.344;
                    _locationRepo.Add(point);
                    if (PointAdded != null)
                    {
                        TimeStampedLocationEventArgs args = new TimeStampedLocationEventArgs { Point = point };
                        PointAdded(this, args);
                    }
                }
            }
            else
            {
                // Always add the first accurate point.
                //
                _locationRepo.Add(point);
                if (PointAdded != null)
                {
                    TimeStampedLocationEventArgs args = new TimeStampedLocationEventArgs { Point = point };
                    PointAdded(this, args);
                }
            }
        }
        if (PositionChanged != null)
        {
            PositionChanged(sender, e);
        }
    }
}

The constructor takes an instance of the ‘IGeoPositionWatcher<GeoCoordinate>’ and ‘ILocationRepository’.

The emulator does not provide simulated data for the GPS hardware. By injecting a fake ‘IGeoPositionWatcher<GeoCoordinate>’  entity into our ‘IGpsService’ this code can still work on the emulator.

The incoming repository may already have points so the constructor initializes the ‘TotalDistanceInMeters’ property to reflect this initial state. Finally the ‘state change’ and ‘position change’ events are wired up.

The bulk of the next bit of code simply exposes properties/methods of the two constructor parameters to the ‘IGpsService’ consumer. The final two methods implement the ‘status change’ and ‘position change’ event handlers. In both cases, the events are raised again and propagate out of the ‘IGpsService’ implementation.

The ‘position change’ event handler first examines the accuracy of the observed GPS point. If the point is accurate enough (20 meters) then the next bit of logic ensures the point is far enough away (0.005 miles) from the last point in the repository. If this final condition is met, the point is added and the ‘PointAdded’ event is raised. This bit of logic prevents the ‘total distance’ from accumulating a bunch of points that consist of GPS error randomly distributed around a static location when ever you stop.

Inversion of Control and the Application

So far, we have built quite a few decoupled components. Normally, I would be leaning on my favorite Inversion of Control (IOC) container, Ninject, to glue all these together. Ninject does have a WP7 compatible release, and I will probably explore this in a later post. However, for now I have a really simple container implementation.

public partial class App
{
    // Dependency Management
    //
    private static IAppSettings _appSettings;
    private static IFileSystem _fileSystem;
    private static ILocationRepository _locationRepo;
    private static MainPageViewModel _mainPageViewModel;
    private static readonly IGeoPositionWatcher<GeoCoordinate> _watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High);
    private static IGpsService _gpsService;

    public static IAppSettings AppSettings
    {
        get { return _appSettings ?? (_appSettings = new AppSettings()); }
    }
    public static IFileSystem FileSystem
    {
        get { return _fileSystem ?? (_fileSystem = new Wp7FileSystem()); }
    }
    public static ILocationRepository LocationRepo
    {
        get { return _locationRepo ?? (_locationRepo = new LocationRepository(FileSystem)); }
    }
    public static MainPageViewModel ViewModel
    {
        get { return _mainPageViewModel ?? (_mainPageViewModel = new MainPageViewModel(GpsService)); }
    }
    public static IGpsService GpsService
    {
        get { return _gpsService ?? (_gpsService = new GpsService(_watcher, LocationRepo)); }
    }

    //
    // Other code removed for brevity ... //
    //
}

In this application all of the components (dependencies) are ‘singleton’ instances. In other words, each has a single instance with the lifetime of the application. The above code simply, exposes a number of public static properties on the Application that allows access to the dependencies. Notice that all the dependencies are hand-wired. The ‘IGpsService’ instance is injected with the ‘GeoCoordinateWatcher’ entity that communicated with the WP7 GPS hardware.

The remaining methods in the application class are stubbed out by the default Visual Studio WP7 solution. These event handlers allow you to take actions when the application is starting / closing or ‘tomb stoning’. The application uses these events for persisting data. The following code shows methods in the Application class that have been modified:

private void Application_Launching(object sender, LaunchingEventArgs e)
{
    // rehydrate the trip data
    LocationRepo.Load();
    object value = AppSettings.Load("Exception");
    ViewModel.Error = value != null ? value.ToString() : "none";
}

private void Application_Activated(object sender, ActivatedEventArgs e)
{
    // rehydrate the trip data
    LocationRepo.Load();
    object value = AppSettings.Load("Exception");
    ViewModel.Error = value != null ? value.ToString() : "none";
}

private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
    // Serialize out the trip data
    LocationRepo.Save();
}

private void Application_Closing(object sender, ClosingEventArgs e)
{
    // Serialize out the trip data
    LocationRepo.Save();
    AppSettings.Remove("Exception");
}

private static void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
    LocationRepo.Save();
    if (System.Diagnostics.Debugger.IsAttached)
    {
        // A navigation has failed; break into the debugger
        System.Diagnostics.Debugger.Break();
    }
}

private static void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
    LocationRepo.Save();
    AppSettings.Save("Exception", e.ExceptionObject.ToString());

    if (System.Diagnostics.Debugger.IsAttached)
    {
        // An unhandled exception has occurred; break into the debugger
        System.Diagnostics.Debugger.Break();
    }
}

This code does two things. First it saves/loads the location data from the repository using the static ‘LocationRepo’ instance when the app is starting/closing. Second is captures unhandled exception data in the isolated storage using the ‘AppSettings’ instance. Capturing unhandled exceptions in this way makes debugging a bit easier on the real hardware once you have installed and are out and about. Otherwise the app will just crash with no trace of what happened.

Model-View-ViewModel

I have attempted to follow the Model-View-ViewModel pattern in this application There is plenty of room for improvement here. If you have suggestions, please contact me or leave them in the comments. The ‘view’ or user interface of the application consists of one page of XAML. The page uses the WP7 Silverlight pivot control to break the content up into four areas. If you have never seen the pivot control, you can visualize it as a tab control where you use a swipe gesture to transition between the tabs. Here is the core main page XAML:

<controls:Pivot x:Name="pivot" Title="Trip Recorder">

    <controls:PivotItem Header="Data">
        <Grid Background="Transparent" Height="752" Width="510" ShowGridLines="False">
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="175"/>
                <ColumnDefinition Width="308*" />
            </Grid.ColumnDefinitions>

            <!--ContentPanel - place additional content here-->
            <Grid Grid.Row="0" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Status: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="0" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding Status}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="1" Grid.Column="0" Margin="12,0,0,0">
                    <TextBlock Text="Timestamp:" Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="1" Grid.Column="1" Margin="0,0,0,0">
                    <TextBlock Text="{Binding TimeStamp}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="2" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Latitude: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="2" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding LatitudeString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="3" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Longitude: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="3" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding LongitudeString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="4" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Altitude: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="4" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding AltitudeString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="5" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Horz Accuracy: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="5" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding HorzAccuracyString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="6" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Vert Accuracy: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="6" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding VertAccuracyString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="7" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Course: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="7" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding CourseString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="8" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Speed: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="8" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding SpeedString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="10" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="Trip Distance: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="10" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding DistanceString}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>
            <Grid Grid.Row="11" Grid.Column="0" Margin="12,0,0,0">
                <TextBlock Text="# Points: " Style="{StaticResource PhoneTextNormalStyle}" TextAlignment="Right" />
            </Grid>
            <Grid Grid.Row="11" Grid.Column="1" Margin="0,0,0,0">
                <TextBlock Text="{Binding NumPoints}" Style="{StaticResource PhoneTextNormalStyle}" />
            </Grid>

            <Grid Grid.Row="12" Grid.Column="0" Grid.ColumnSpan="2">
                <StackPanel>
                    <Button x:Name="Start" Height="120" Click="Toggle_Click" Background="#B0088312" Content="Start" Width="400" />
                    <Button x:Name="Stop" Height="120" Click="Toggle_Click" Background="#94FF0000" Content="Stop" Width="400" Visibility="Collapsed" />
                    <Button x:Name="Clear" Background="#AC0000FF" Height="120" Click="Clear_Click" Content="Clear" Width="400" />
                </StackPanel>
            </Grid>
        </Grid>
    </controls:PivotItem>

    <controls:PivotItem Header="Map">
        <StackPanel>
            <my:Map Height="544" Name="map1" Width="482"
                    Center="{Binding MapCenter}" ZoomLevel="{Binding MapZoom}"
                    CredentialsProvider="enter-your-own-bing-maps-developer-id"
                    MouseLeftButtonDown="Map1MouseLeftButtonDown">
                <my:Map.Mode>
                    <my:AerialMode ShouldDisplayLabels="True" />
                </my:Map.Mode>
                <my:MapPolyline Locations="{Binding Locations}" StrokeThickness="5" Opacity="0.65" Stroke="Blue"></my:MapPolyline>
                <my:Pushpin Location="{Binding Position}" Background="Orange"></my:Pushpin>
            </my:Map>
            <TextBlock Text="{Binding Address}" Style="{StaticResource PhoneTextNormalStyle}" />
        </StackPanel>
    </controls:PivotItem>

    <controls:PivotItem Header="Settings">
        <StackPanel>
            <TextBlock Text="Units" FontSize="28" FontWeight="Bold"></TextBlock>
            <RadioButton x:Name="Imperial" GroupName="Unit" Content="Imperial (miles, feet, mph)" IsChecked="True" Checked="Imperial_Checked"></RadioButton>
            <RadioButton x:Name="Metric" GroupName="Unit" Content="Metric (kilometers, meters, kmph)" Checked="Metric_Checked"></RadioButton>
        </StackPanel>
    </controls:PivotItem>

    <controls:PivotItem Header="Error">
        <StackPanel>
            <TextBlock Text="{Binding Error}" Style="{StaticResource PhoneTextNormalStyle}" />
        </StackPanel>
    </controls:PivotItem>

</controls:Pivot>

The pivot control has the following main items:

  • Data – Presents raw GPS / trip summary data and exposes the start/stop/clear controls.
  • Map – Displays the Bing Maps Silverlight control and the line/pushpin overlays.
  • Settings – Presents the user the choice of either ‘metric’ or ‘imperial’ units.
  • Error – A temporary page that displays exception data.

A few comments on the XAML. First, in the spirit of MVVM, most of the dynamic content is set by ‘binding’ a control’s property to a property on the view-model (to be discussed shortly). Second, the ‘Map’ control has a ‘CredentialProviders’ property that you will need to fill out with your Bing ID assigned to your application.

The following is the code-behind for the above XAML:

public partial class MainPage
{
    private readonly IGpsService _gpsService = App.GpsService;

    private Point _p1, _p2;
    private bool _isZooming;
    private bool _isLoaded;
    private bool _isStarted;

    // Constructor
    public MainPage()
    {
        InitializeComponent();
        Touch.FrameReported += Touch_FrameReported;

        DataContext = App.ViewModel;
    }

    private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
    {
        if (!_isLoaded)
        {
            return;
        }
        // If there are more than one finger on screen
        if (!pivot.IsHitTestVisible && e.GetTouchPoints(map1).Count == 2)
        {
            TouchPointCollection tpc = e.GetTouchPoints(map1);

            // if true that means this is the first time the user puts its fingers on the screen in order to perform a zoom in/out
            if (!_isZooming)
            {
                // store the starting segment
                _p1 = tpc[0].Position;
                _p2 = tpc[1].Position;
                _isZooming = true;
            }
            else
            {
                // store the secondary segment
                Point tp1 = tpc[0].Position;
                Point tp2 = tpc[1].Position;

                //compute the segments distances then subtract them from each other and add the result to the zoom
                double d1 = Math.Sqrt(Math.Pow(tp1.X - tp2.X, 2) + Math.Pow(tp1.Y - tp2.Y, 2));
                double d2 = Math.Sqrt(Math.Pow(_p1.X - _p2.X, 2) + Math.Pow(_p1.Y - _p2.Y, 2));
                double distance = d1 - d2;
                _isZooming = true;

                // we divide the zoom ratio by 10 to keep it slow, but you can choose any value that suite your needs
                map1.ZoomLevel += (distance / 20);
                _p1 = tp1;
                _p2 = tp2;
            }
        }
        else
        {
            _isZooming = false;
        }
    }

    private void Map1MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        pivot.IsHitTestVisible = false;
    }

    private void LayoutRoot_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        pivot.IsHitTestVisible = true;
    }

    private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
    {
        LoadSettings();
        _isLoaded = true;
    }

    private void Imperial_Checked(object sender, RoutedEventArgs e)
    {
        SaveSettings();
        App.ViewModel.IsMetric = false;
    }

    private void Metric_Checked(object sender, RoutedEventArgs e)
    {
        SaveSettings();
        App.ViewModel.IsMetric = true;
    }

    private void SaveSettings()
    {
        if(Imperial==null)
        {
            return;
        }
        const string name = "Unit";
        string value;
        if (Imperial.IsChecked.HasValue && Imperial.IsChecked.Value)
        {
            value = "Imperial";
        }
        else
        {
            value = "Metric";
        }
        App.AppSettings.Save(name, value);
    }

    private void LoadSettings()
    {
        const string name = "Unit";
        object value = App.AppSettings.Load(name);
        if (value != null)
        {
            string unit = (string) value;
            if (unit == "Imperial")
            {
                Imperial.IsChecked = true;
                Metric.IsChecked = false;
            }
            else
            {
                Imperial.IsChecked = false;
                Metric.IsChecked = true;
            }
        }
    }

    private void Toggle_Click(object sender, RoutedEventArgs e)
    {
        if(!_isStarted)
        {
            _isStarted = true;
            Start.Visibility = Visibility.Collapsed;
            Stop.Visibility = Visibility.Visible;
            _gpsService.Start();
        }
        else
        {
            _isStarted = false;
            Stop.Visibility = Visibility.Collapsed;
            Start.Visibility = Visibility.Visible;
            _gpsService.Stop();
        }
    }

    private void Clear_Click(object sender, RoutedEventArgs e)
    {
        _gpsService.Clear();
        _gpsService.Save();
    }
}

 

There is still some improvements that can be done using ‘ICommand’ and binding the radio buttons. I will save those improvements for possible a later post.

First notice we use the ‘IOC container’ we previously build to get the ‘IGpsService’ instance. Second notice we set the ‘DataContext’ in the constructor to the instance of the ‘ViewModel’ (again using the IOC container we built).

Embedding the map control into the pivot control presents a bit of a dilemma. Both controls intercept on use the ‘flick touch motion’. By default it appears that the pivot control wins. The solution presented in the previous link works. I have tweaked it to zoom as expected (in when the fingers separate) and adjusted the zoom sensitivity a bit.  The ‘Touch_FrameReport’, ‘Map1MouseLeftButtonDown’, and ‘LayouRoot_MouseLeftButtonUp’ event handlers work together to disable (by setting the ‘IsHitTestVisible’ to false) pivot control when the map has focus.

The ‘PhoneApplicationPage_Loaded’, ‘Imperial_Checked’, ‘Metric_Checked’, ‘SaveSettings’, and ‘LoadSettings’ methods handle the two radio button events on the ‘Units’ pivot item. Finally the ‘Toggle_Click’ and ‘Clear_Click’ event handlers manage the start/stop and clear button behaviors.

The only thing left to discuss is the view-model. The view-model must implement the ‘INotifyPropertyChange’ interface to allow properties that are bound to the view to supply a ‘change notification’ to the view (and this can be two-way) to trigger an update. The following base class provides this implementation:

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnNotifyPropertyChanged(string p)
    {
        if(PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(p));
        }
    }

    public bool IsDesignTime
    {
        get
        {
            return (Application.Current == null) || (Application.Current.GetType() == typeof (Application));
        }
    }
}

The view-model implementation in this version is a bit heavy and deserves a round (or maybe two) of refactoring. The private ‘backing’ variables for the properties and implementation of the properties for binding account for most of the line count. Here is the code:

public class MainPageViewModel : ViewModelBase
{
    private readonly IGpsService _gpsService;

    // These all have public properties and are exposed to the
    //  view for binding.
    //
    private readonly LocationCollection _locations = new LocationCollection();
    private string _status;
    private string _timestamp;
    private double _latitude;
    private double _longitude;
    private double _altitude;
    private double _horzAccuracy;
    private double _vertAccuracy;
    private double _course;
    private double _speed;
    private double _distance;
    private string _address;
    private string _error;
    private bool _isMetric;
    private GeoCoordinate _mapCenter;
    private double _mapZoom;

    public MainPageViewModel(IGpsService gpsService)
    {
        // Wire up event handlers to the service
        //  events so the model can reflect the current state.
        //
        _gpsService = gpsService;
        _gpsService.StatusChanged += GpsServiceStatusChanged;
        _gpsService.PositionChanged += GpsServicePositionChanged;
        _gpsService.PointAdded += GpsServicePointAdded;

        // If there are existing points, then we need to set
        //  the state accordingly.
        //
        if(_gpsService.Count > 0)
        {
            foreach (TimeStampedLocation location in _gpsService.Locations)
            {
                _locations.Add(location.Location);
            }
            OnNotifyPropertyChanged("Locations");
            OnNotifyPropertyChanged("Position");
            OnNotifyPropertyChanged("NumPoints");
            MapCenter = _locations[_locations.Count - 1];
            MapZoom = 15.0;
            Distance = _gpsService.TotalDistanceInMeters;

            // Fetch the address from the reverse geocoding service.
            //
            ReverseGeocode(Position.Latitude, Position.Longitude);
        }
    }

    #region Binding Points

    public string Status
    {
        get { return _status; }
        set
        {
            if(_status != value)
            {
                _status = value;
                OnNotifyPropertyChanged("Status");
            }
        }
    }

    public GeoCoordinate Position
    {
        get { return Locations.Count == 0 ? null : Locations[Locations.Count - 1]; }
    }

    public string TimeStamp
    {
        get { return _timestamp; }
        set
        {
            if(_timestamp!=value)
            {
                _timestamp = value;
                OnNotifyPropertyChanged("TimeStamp");
            }
        }
    }

    public double Latitude
    {
        get { return _latitude; }
        set
        {
            if(_latitude != value)
            {
                _latitude = value;
                OnNotifyPropertyChanged("Latitude");
                OnNotifyPropertyChanged("LatitudeString");
            }
        }
    }
    public string LatitudeString
    {
        get { return string.Format("{0:0.00} degrees", _latitude); }
    }

    public double Longitude
    {
        get { return _longitude; }
        set
        {
            if(_longitude !=value)
            {
                _longitude = value;
                OnNotifyPropertyChanged("Longitude");
                OnNotifyPropertyChanged("LongitudeString");
            }
        }
    }
    public string LongitudeString
    {
        get { return string.Format("{0:0.00} degrees", _longitude); }
    }

    public double Altitude
    {
        get { return _altitude; }
        set
        {
            if(_altitude != value)
            {
                _altitude = value;
                OnNotifyPropertyChanged("Altitude");
                OnNotifyPropertyChanged("AltitudeString");
            }
        }
    }
    public string AltitudeString
    {
        get
        {
            double val = ConvertUnitsSmall(_altitude);
            if (_isMetric)
            {
                return string.Format("{0:0.00} meters", val);
            }
            return string.Format("{0:0.00} feet", val);
        }
    }

    public double HorzAccuracy
    {
        get { return _horzAccuracy; }
        set
        {
            if(_horzAccuracy != value)
            {
                _horzAccuracy = value;
                OnNotifyPropertyChanged("HorzAccuracy");
                OnNotifyPropertyChanged("HorzAccuracyString");
            }
        }
    }
    public string HorzAccuracyString
    {
        get
        {
            double val = ConvertUnitsSmall(_horzAccuracy);
            if (_isMetric)
            {
                return string.Format("{0:0.00} meters", val);
            }
            return string.Format("{0:0.00} feet", val);
        }
    }

    public double VertAccuracy
    {
        get { return _vertAccuracy; }
        set
        {
            if(_vertAccuracy != value)
            {
                _vertAccuracy = value;
                OnNotifyPropertyChanged("VertAccuracy");
                OnNotifyPropertyChanged("VertAccuracyString");
            }
        }
    }
    public string VertAccuracyString
    {
        get
        {
            double val = ConvertUnitsSmall(_vertAccuracy);
            if (_isMetric)
            {
                return string.Format("{0:0.00} meters", val);
            }
            return string.Format("{0:0.00} feet", val);
        }
    }

    public double Course
    {
        get { return _course; }
        set
        {
            if(_course!=value)
            {
                _course = value;
                OnNotifyPropertyChanged("Course");
                OnNotifyPropertyChanged("CourseString");
            }
        }
    }
    public string CourseString
    {
        get { return string.Format("{0:0.00} degrees", _course); }
    }

    public double Speed
    {
        get { return _speed; }
        set
        {
            if(_speed!=value)
            {
                _speed = value;
                OnNotifyPropertyChanged("Speed");
                OnNotifyPropertyChanged("SpeedString");
            }
        }
    }
    public string SpeedString
    {
        get
        {
            double val = ConvertUnitsLarge(_speed)*60*60;
            if(_isMetric)
            {
                return string.Format("{0:0.00} kmph", val);
            }
            return string.Format("{0:0.00} mph", val);
        }
    }

    public int NumPoints
    {
        get { return Locations.Count; }
    }

    public double Distance
    {
        get { return _distance; }
        set
        {
            if(_distance != value)
            {
                _distance = value;
                OnNotifyPropertyChanged("Distance");
                OnNotifyPropertyChanged("DistanceString");
            }
        }
    }
    public string DistanceString
    {
        get
        {
            double val = ConvertUnitsLarge(_distance);
            if(_isMetric)
            {
                return string.Format("{0:0.00} kilometers", val);
            }
            return string.Format("{0:0.00} miles", val);
        }
    }

    public LocationCollection Locations
    {
        get { return _locations; }
    }

    public string Address
    {
        get { return _address; }
        set
        {
            if(_address != value)
            {
                _address = value;
                OnNotifyPropertyChanged("Address");
            }
        }
    }

    public bool IsMetric
    {
        get { return _isMetric; }
        set
        {
            if(_isMetric != value)
            {
                _isMetric = value;
                OnNotifyPropertyChanged("IsMetric");
                OnNotifyPropertyChanged("AltitudeString");
                OnNotifyPropertyChanged("HorzAccuracyString");
                OnNotifyPropertyChanged("VertAccuracyString");
                OnNotifyPropertyChanged("SpeedString");
                OnNotifyPropertyChanged("DistanceString");
            }
        }
    }

    public GeoCoordinate MapCenter
    {
        get { return _mapCenter; }
        set
        {
            if(_mapCenter != value)
            {
                _mapCenter = value;
                OnNotifyPropertyChanged("MapCenter");
            }
        }
    }

    public double MapZoom
    {
        get { return _mapZoom; }
        set
        {
            if(_mapZoom != value)
            {
                _mapZoom = value;
                OnNotifyPropertyChanged("MapZoom");
            }
        }
    }

    public string Error
    {
        get { return _error; }
        set
        {
            if(_error != value)
            {
                _error = value;
                OnNotifyPropertyChanged("Error");
            }
        }
    }

    #endregion

    private void GpsServicePointAdded(object sender, TimeStampedLocationEventArgs e)
    {
        // A point was added to the collection of trip points.
        //

        // Add the location and notify the view
        //
        Locations.Add(e.Point.Location);
        OnNotifyPropertyChanged("Locations");
        OnNotifyPropertyChanged("Position");
        OnNotifyPropertyChanged("NumPoints");

        // If this is the first point, provide a default zoom
        //  and set the center.
        //
        if (Locations.Count == 1)
        {
            MapCenter = e.Point.Location;
            MapZoom = 15.0;
        }

        // Update the total trip distance.
        //
        Distance = _gpsService.TotalDistanceInMeters;

        // Fetch the address from the reverse geocoding service.
        //
        ReverseGeocode(e.Point.Location.Latitude, e.Point.Location.Longitude);
    }

    private void GpsServicePositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)
    {
        // A new observation point was collected by the GPS.
        //  This was not necessarily with enough accuracy or
        //  far enough away from the last trip point.
        //
        // Update the properties and notify the UI
        //
        TimeStamp = e.Position.Timestamp.ToString();
        Latitude = e.Position.Location.Latitude; // degrees
        Longitude = e.Position.Location.Longitude; // degrees
        Altitude = e.Position.Location.Altitude; // meters
        HorzAccuracy = e.Position.Location.HorizontalAccuracy; // meters
        VertAccuracy = e.Position.Location.VerticalAccuracy; // meters
        Course = e.Position.Location.Course; // degrees relative to north
        Speed = e.Position.Location.Speed; // meters per second
    }

    private void GpsServiceStatusChanged(object sender, GeoPositionStatusChangedEventArgs e)
    {
        // A status change in the GPS receiver.
        //  Update and notify the UI.
        //
        Status = e.Status.ToString();
    }

    private void ReverseGeocode(double lat, double lng)
    {
        // Reverse geocode using the Bing service.
        //
        ReverseGeocodeRequest reverseGeocodeRequest = new ReverseGeocodeRequest
                                                          {
                                                              Credentials = new Credentials
                                                                                {
                                                                                    ApplicationId = Keys.BingMaps
                                                                                }
                                                          };

        Location point = new Location { Latitude = lat, Longitude = lng };
        reverseGeocodeRequest.Location = point;

        try
        {
            GeocodeServiceClient geocodeServiceClient = new GeocodeServiceClient("BasicHttpBinding_IGeocodeService");
            geocodeServiceClient.ReverseGeocodeCompleted += GeocodeServiceClientReverseGeocodeCompleted;
            geocodeServiceClient.ReverseGeocodeAsync(reverseGeocodeRequest);
        }
        catch (Exception ex)
        {
            // Could be the revsese geocode service is off-line
            ReverseGeocodeCompletedEventArgs fake = new ReverseGeocodeCompletedEventArgs(null, ex, false, null);
            GeocodeServiceClientReverseGeocodeCompleted(this, fake);
        }
    }

    private void GeocodeServiceClientReverseGeocodeCompleted(object sender, ReverseGeocodeCompletedEventArgs e)
    {
        // Asnyc callback for completed Bing reverse geocode.
        //
        GeocodeResponse geocodeResponse = e.Result;
        Address = geocodeResponse.Results.Length > 0 ? geocodeResponse.Results[0].DisplayName : "not found";
    }

    private double ConvertUnitsSmall(double value)
    {
        if (!IsMetric)
        {
            const double conversionFactorFeetPerMeter = 3.2808399;
            return value * conversionFactorFeetPerMeter;
        }
        return value;
    }

    private double ConvertUnitsLarge(double value)
    {
        if (!IsMetric)
        {
            const double conversionFactorMilesPerMeter = 0.000621371192;
            return value * conversionFactorMilesPerMeter;
        }
        return value * 0.001;
    }
}

The constructor takes an ‘IGpsService’ instance. This allows the view-model to bind to the three exposed events and update various properties when these events are raised. There is a bit of logic in the constructor to account for any existing locations by setting the view-model properties appropriately.

Next is a long list of property implementations that are bound to the various XAML elements. The XAML elements are bound to a ‘String’ representation of the underlying element. For example, the binding uses the ‘SpeedString’ property instead of the ‘Speed’ property directly. This allows the value in the ‘Speed’ property to be displayed in the currently selected unit (metric or imperial).

Below this region are the three ‘IGpsService’ event handlers. As new data arrives (via the event) properties are set and ‘OnNotifyPropertyChanged’ methods are called to alert the view. New points are also sent off to the Bing reverse geocoding service to determine the address.

Summary

Sorry for the long post. We covered a lot of ground. I thought about splitting this into a couple of posts, but couldn’t settle in good way to divide it up. Developing WP7 applications is very enjoyable to a large extent because of the tools that have been provided by Microsoft. I am certain these will only get better. As always, if you have constructive criticism, questions or want to leave a comment please do so.

Posted in geolocation, wp7 | 3 Comments

Queuing Theory and the Production Pipeline

Project management is not a hard science and often deals with many aspects that are not well defined. However, applying a bit of science / math can often lead to new insights and more efficient processes. In this post, I will apply a touch of queuing theory to project management.

I do not consider my self an expert in this area. I am a strong agile proponent and a practicing certified scrum master. I am continually learning and improving my understanding. Please read this with a bit of skepticism and provide constructive feedback / debate in the comment section.

Queuing Theory

Many times in a project a team will have a list or a queue of tasks they need to complete. This team could be a group of engineers analyzing customer requirements, a few developers implementing the requirements, or a crew of testers. Here is a back of the envelope (or napkin) picture of a queue.

queue_1

In this case the queue depicts ‘N’ items. These N items define the scope of the work or the ‘back log’. The team will work on the N items and complete them all in ‘T’ time. Many times you will hear the term ‘burn down’ to refer to this activity. The team establishes a consistent rate ‘R’ of burning down queued items. The rate ‘R’ is the sum of all the individual contributor rates ‘r’. The rate ‘R’ is directly proportional to how well funded the project is.

The ‘scope-time-cost’ or ‘good-fast-cheap’ relationships hold. The business can choose only choose two. Therefore:

  • If the scope ‘N’ and time ‘T’ are fixed, then the project needs the appropriate funding (resources) to create the desired burn down rate ‘R’.
  • If the scope ‘N’ and the resources ‘R’ are fixed, then the queue will be emptied at the time ‘T’.
  • If the resources ‘R’ and time ‘T’ are fixed, then the scope ‘N’ is what you can afford.

Risk Management – Queues

Controlling risk is an important activity in project management. There are risks that can impact scope ‘N’, time ‘T’ and burn down rate ‘R’. One of the largest risks that a queue has is that queued items no longer represent customer needs. Items in the queue have a shelf life. Imagine the items represent items to be implemented by a development team. These items originated from customer requirements that have been analyzed and flushed out by the requirements team. What is the shelf life for these items? Is it one year? Six months? Probably not. Many industries move much faster than this. For some industries even a one month shelf life is too long.

So what can be done to mitigate this shelf life risk? The answer is simple. Keep the queue prioritized and only stock the queue with just enough items that match the capacity of your team.

num_to_stock_1 

This equation represents the maximum number of items that can be queued without exceeding the shelf life. The ideal mitigation of this risk is to push this towards a ‘just-in-time’ queuing system. In lean software engineering ‘waste’ is defined as ‘everything not adding value to the customer’. Queued items represent ‘waste’. Just-in-time queues minimize the waste in the system.

Chained Queues

It is very common that the work of burning down items on one queue feeds another queue. This forms a production pipeline that items work their way down. For example here is a three stage queue forming a production pipeline:

chained_1

Each queue will resource a group of individuals with a specialized skill set. Often the number of items in the down stream queues is larger than the previous queues. For example, a single customer requirement may result in multiple system requirements. One system requirement may have multiple tests to ensure functionality under various conditions. Downstream queues don’t have to wait for upstream queues to complete their backlog before proceeding. In fact, there are benefits to not doing this as we will see.

The number of queues may vary depending upon the specific software development process. For the sake of discussion, I will continue forward assuming the three queues above.

Risk Management – Chained Queues

The final customer does not need to know the details of how many queues exist, what they are and their order. They are concerned with what enters and exits the production pipeline. This customer provides an initial voice (voice of the customer) and expects a product out the other end on time and in budget. The probability of meeting this expectation is inversely proportional to how much time it takes to turn the ‘voice of the customer’ into product. To make things more difficult, customers often change their expectations after the initial requirements have been gathered.

To mitigate this risk, constant communication with the customer is necessary. Expecting change and providing opportunities to change is a common agile principle. This will be discussed further when below.

Managed Flow – Waterfall

The flow of items down the chain of queues (or the production pipeline) can become a problem. An improperly managed pipeline can result in teams either being too far under or too far over capacity. Here is an example of a pipeline where each queue fully empties before the next queue begins.

waterfall_2

This type of flow is commonly known as ‘waterfall’ or ‘phased gate’ approach. The ‘requirements phase’ must be completed before the ‘development phase’ begins and so on. This can lead to a down stream queues that rapidly exceed their shelf life and resource capacity. The items flow down the pipeline in a big lump. Resource consumption (capacity management) is also lumpy.

An area of ‘friction’ in the pipe often exists around the ‘hand off’ areas. The requirements engineers have been analyzing and producing system requirements for months. The shear number of system requirements cannot easily be provided to the developers in a clear and concise manner.

The customer is provided an inspection opportunity when the final queue is empty (T1 + T2 + T3). That is, when the final product is completed. At this time, change is the most expensive.

Managed Flow – Agile

A different way to manage the pipeline flow is to develop the product iteratively using a regular cadence. During each interval a portion of the original requirements are developed as fully as possible. Here is a pictorial of this scenario where the original requirements are broken down into thirds:

agile_2

In this scenario, the customer (or a customer representative) prioritizes the full backlog of N1 items. The requirements group then pulls the top one-third (N1/3) items, analyzes and create system requirements. The difference is that the developers do not wait until all N1 requirements are analyzed. They only wait for the first one-third.

In this case, one-third was taken as an example. Most of the time the pipeline team (requirements + devs + testers in this case) determine a maximum queue capacity or a work in progress (WIP) limit. The team then pulls from the original requirement just enough to fill the queue. This allows the items in each queue to be processed before their shelf life expires and guarantees the capacity of the individual teams will not exceed their maximum.

In the above image, the flow along the pipeline can easily be visualized. Now instead of one large lump of items, there are many smaller lumps. Each lump still traverses the full pipeline. The pipeline flow and resource consumption have a very regular cadence. The cadence is typically set be establishing a time limit (time box) for each interval. That in turn determines the WIP limit. Having a regular cadence allows resources (capacity) to be managed more effectively.

The work exiting the pipeline is an incremental improvement to the product. These incremental improvements allow the product to evolve and until a shippable version of the product exists. These incremental improvements also provide inspection points. The customer has the opportunity to provide feedback at a regular interval. This promotes constant communication and provides a mechanism for the customer to introduce change. This mitigates the cost of change by allowing the customer to ‘steer the boat’ early and often.

Cooperative Flow

The above scenario depicts the development team accepting a queue from the requirements team without any overlap. In reality, there is generally some overlap as the development team ‘comes up to speed’ with the new items. Similarly, the testing group will likely need some development (and probably some requirements) time to create a comprehensive test plan. The ‘burn-down’ for any given iteration will likely be something as follows:

reality_2 The requirements engineers are at full capacity during the period where heavy analysis is occurring. However, there is some additional draw on the requirements engineers during the development and testing phases. Likewise the other teams will likely have some draw upon their resources during all times with a spike during the period when the items are in their queue.

Having the full team (representatives from each queue) resourced for the full iteration is beneficial to the final quality of the product. For example, during what is normally considered ‘requirements engineering’ (or when most of the items are queued in the requirements backlog) developers can provide insight into an implementation detail that may effect the system requirement. Likewise, a test engineer can help ensure the system requirement is testable. During the primarily ‘development phase’ the requirements engineers can provide further clarity and the test engineers can help promote testable code.

This cooperation flow leads to a better product. Miscommunication that leads to rework is averted early. The final product better represents the original customer vision. The code base is better able to be tested (hopefully automatically).

Summary

Visualizing the software development process as a pipeline of queues offers a model that allow queuing theory to be applied. This provides many insights into how to manage capacity and mitigate risk. The model developed above is a bit ideal and will surely not fit every situation. This model provides a fundamental base. This model can help explain the benefits / costs of the various project management methodologies (waterfall, scrum, lean). As always, I would love to hear what you think in the comments.

Posted in best practice, engineering | 2 Comments

A Persistent ASP.NET Session Manager

I recently had a need for an ASP.NET Session that lasted longer that a normal web session (one day) and was able to survive application restarts (or IIS restarts). I know there are other solutions that exist. Some involve storing the Session State in a database. Adding to my database schema to store the Session State data seemed a bit over kill. The data I am storing is small and would normally be held in-memory (like the normal ASP.NET Session State). I just wanted to add a bit of ‘life-after-a-restart’ feature to the normal in-memory Session.

Session Domain Model

Before looking at the implementation, let’s take a look at the data that I am storing in this Session Manager. I wanted a typed object that new how to re-hydrate / serialize itself from / to the Session. The following code is the typed object:

public class TripSession
{
    public Guid Id { get; set; }
    public DateTime Expiration { get; set; }
    public string UserName { get; set; }
    public int TruckId { get; set; }

    public override string ToString()
    {
        return Id + "\t" + Expiration + "\t" + UserName + "\t" + TruckId;
    }

    public static TripSession ParseLine(string line)
    {
        string[] parts = line.Split('\t');
        if(parts.Length!=4)
        {
            return null;
        }
        try
        {
            TripSession tripSession = new TripSession
                                          {
                                              Id = new Guid(parts[0]),
                                              Expiration = DateTime.Parse(parts[1]),
                                              UserName = parts[2],
                                              TruckId = int.Parse(parts[3])
                                          };
            return tripSession;
        }
        catch (Exception)
        {
            return null;
        }
    }
}

As you can see this object has four public properties. In addition the ‘ToString’ has an override that serializes these properties into tabbed-separated string. Finally, there is a static method that takes a tabbed separated string and re-hydrates it into an object.

Session Manager

The higher level class that manages the ‘TripSession’ objects is shown in the following code:

public class TripSessions
{
    private const string _relativeSesionFile = "~/App_Data/TripSessions.txt";
    private readonly string _sessionFile;
    private readonly ILogger _logger;
    private readonly Dictionary<Guid, TripSession> _sessions;
    private readonly TimeSpan _maxAge = new TimeSpan(1, 0, 0, 0);

    public TripSessions(ILogger logger, HttpContextBase httpContextBase)
    {
        _logger = logger;

        _sessionFile = httpContextBase.Server.MapPath(_relativeSesionFile);

        _sessions = ReadSessionFile();
    }

    public TripSession CreateSession(string userName, int truckId)
    {
        try
        {
            TripSession tripSession = new TripSession
                                          {
                                              Id = Guid.NewGuid(),
                                              Expiration = DateTime.Now + _maxAge,
                                              TruckId = truckId,
                                              UserName = userName
                                          };
            _sessions[tripSession.Id] = tripSession;
            SaveSessionFile();
            _logger.Debug("Created session for: username=" + userName + ",truckid=" + truckId);
            return tripSession;
        }
        catch (Exception ex)
        {
            _logger.Error("Failed to create session. ", ex);
        }
        return null;
    }

    public TripSession GetSession(Guid id)
    {
        if(_sessions.ContainsKey(id))
        {
            return _sessions[id];
        }
        return null;
    }

    private void SaveSessionFile()
    {
        _logger.Debug("Saving trip session data to file.");
        List<string> lines = new List<string>();
        foreach (KeyValuePair<Guid, TripSession> keyValuePair in _sessions)
        {
            TripSession tripSession = keyValuePair.Value;
            lines.Add(tripSession.ToString());
        }
        File.WriteAllLines(_sessionFile, lines.ToArray());
    }

    private Dictionary<Guid, TripSession> ReadSessionFile()
    {
        _logger.Debug("******READING TRIP SESSION FILE**********");
        Dictionary<Guid, TripSession> result = new Dictionary<Guid, TripSession>();

        if(!File.Exists(_sessionFile))
        {
            _logger.Debug("The session file does not exist. file=" + _sessionFile);
            return result;
        }

        string[] lines = File.ReadAllLines(_sessionFile);
        foreach (string line in lines)
        {
            TripSession tripSession = TripSession.ParseLine(line);
            if (tripSession != null && (DateTime.Now - tripSession.Expiration) < _maxAge)
            {
                result[tripSession.Id] = tripSession;
                _logger.Debug("ADDED---->" + line);
            }
            else
            {
                _logger.Debug("EXPIRED-->" + line);
            }
        }
        return result;
    }
}

One instance of the ‘TripSession’ class is created by my IOC container (Ninject). In essence the ‘TripSession’ entity is a “singleton”. The constructor of the ‘TripSessions’ class accepts two dependencies (again both supplied by my IOC container). The ‘ILogger’ instance is used for logging and the ‘HttpContextBase’  is used to resolve the relative path of the file used to store the session data. Once the relative path has been resolved, the session data is loaded from the disk by calling the private ‘ReadSessionFile’ method. Because this object is a “singleton” this reading from disk only occurs during application startup.

The ‘ReadSessionFile’ creates a Dictionary with a Guid key and a TripSession as the value. The code then populates this dictionary by processing each line in the session file. A bit of logic is applied to enforce an expiration (one day) of the TripSession objects. This result is then used by the class as an in-memory data store of TripSession objects (via the ‘_sessions’ field).

New ‘TripSession’ object are created by calling the ‘CreateSession’ method. An expiration date is applied to the object as it is created. To insure the new data will persist across restarts, the data is then serialized to disk (by calling ‘SaveSessionFile’). The ‘SaveSessionFile’ method simply accumulates all the current TripSession data into a list of strings which is then serialized out to the data file.

The remaining method, ‘GetSession’, takes a Guid as a parameter, finds the object in the dictionary and returns the instance. Otherwise it returns null.

Summary

The Session Manager above will persist session data across application (and IIS) restarts. Being file based has advantages and disadvantages which you will need to weigh in your design decision. I minimized the disk thrashing by reading and writing only win necessary. I would be interested to hear your solutions to this problem. How do you persist Session data across application restarts?

Posted in asp.net, c# | 6 Comments

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.

Posted in asp.net, geolocation, jQuery, javascript, mvc | 3 Comments

Migrating from GoDaddy Shared Hosting to VDS

I feel like my blogging activity lately has come to a screeching halt. What has been keeping me busy you ask? Well I have been hosting my sites (including this blog) on shared hosting accounts with GoDaddy. The shared hosting accounts are okay, but you don’t have any control over many configuration options (e.g. medium trust is not optional) and many troubleshooting tools (e.g. IIS event logs). I finally decided to take the plunge and move up to a GoDaddy virtual dedicated server (VDS).

Yes, I know. Many of you just threw up a little in your mouth. GoDaddy has earned a bad reputation with many of people. My experience has been two-fold. On one side, I have been very happy with the performance of my shared hosting sites. The hardware and the internet connectivity isn’t bad. The sales department was also very helpful. They really helped guide my VDS selection (more later).

The other side of the coin, is getting support can be very frustrating. Many times, after a frustrating conversation with someone who seemed to be running from a script, I end up solving my own issues. You better be able to manage your own configuration. GoDaddy has plans where you can pay for service. If you feel comfortable setting up IIS, configuring sites, administering email…etc then you will be fine.

VDS Specs / Cost

From the GoDaddy menu of virtual dedicated servers I selected their Windows ‘Value’ package. To summarize the specs:

  • Windows Server 2008 Standard 64 bit
  • 2GB RAM
  • 30 GB Storage
  • 1000 GB/month Bandwidth

This is plenty of a web server to handle the traffic for my domains. The sales staff eased my mind a bit indicating that I can easily expand later (if necessary). The cost for this server is just over $30 US per month. Compared to the $6 US per month for the shared hosting. Considering that I am sharing this cost over a few domains and offsetting it a bit more by running ads on my blog, the bump in cost is worth being able to fully manage the server.

The VDS is a virtual computer instance hosted by GoDaddy. You have total control over the server. You can remote desktop into it and install what you want. It is yours to use or abuse. GoDaddy does provide you with the ability to ‘re-provision’ your server by clicking a link in your management panel (on the web). Behind the scenes this queues up a re-paving of your server back to the bare-bones image. The re-provisioning service only takes about an hour (my experience) and is nice in the beginning when you are trying out various configurations.

Plesk Control Panel

The biggest mistake I made was including the ‘Plesk’ control panel. The ‘Plesk’ panel is a control panel that provides a user interface (web based) that allows you to create new web sites, administer your server (web and email) and many other tasks. It does exactly what it is advertized to do. However, I was not happy with the underlying folder structure for the added sites. Seemed to be using some convention that rubbed me the wrong way. I have turned this ‘feature’ off.

Email

Email is a bit tricky. GoDaddy requires all out-going emails to go through their relay mail server. They put this there to meter the quantity of emails that you send. Presumably to prevent you from creating a spamming site on their servers (remember you only rent your VDS). I initially thought I would set up an email server and admin my own emails. Very quickly I came to the conclusion that I did not want to spend my time doing that. I switched all my sites over to Google Applications. Google really does this well and I recommend them.

Transferring Content

Setting up the new server is mostly transferring content from the old server to the new one. I have been doing this by RDP’ing into the VDS and using an FTP client to the old site. Once the content was on the new server, add and configure the new domain in IIS7. Finally, configure the DNS to point at the new server’s IP address. Repeat for all your sites. This was straight-forward and most of the time this worked without issue.

When transferring WordPress blogs (and other applications with databases) export the content from the database on the old server into a script (.SQL) file. Then import that content into the database on the new server and reconfigured the database connection strings. Again, this was usually straight-forward.

Summary

I have been transferring the content slowly (lack of time on my part) and am pleased to announce the content is fully transferred and all sites are up and running on the new server. Migrating to the new server was about what I expected. Hopefully this means I can get back to more technical blogging soon.

Posted in GoDaddy, hardware | 17 Comments

Using Ninject To Manage Critical Resources

This post does not pertain to the most recent (shortly after the release of this post) versions of Ninject. Ninject now handles the deactivation of all objects bound ‘InRequestScope’.

Recently a discussion with a reader led to this Stack Overflow question. Here is a summary of the problem:

The result is that the application eventually cannot connect to the database and fails.

The ‘InRequestScope’ binding ties the lifetime of the object to the instance of ‘HttpContext.Current’. Ninject deactivates objects when the object they are tied to gets garbage collected. Ninject uses an instance of a ‘WeakReference’ object coupled to a timer to determine if the garbage collector has run. It then purges the cached objects. A thorough post about Ninject’s lifecycle management was written previously by Nate Kohari.

I set up a test project to investigate a bit. I put a break point on the ‘Dispose’ method for the UnitOfWork instance. I then banged on the site by continually clicking refresh. I found the number of open database connections did indeed increase linearly with every request. The ‘Dispose’ break point was never reached. I then modified my code to include the following in my web application’s ‘EndRequest’ event:

void NinjectWebsiteApplication_EndRequest(object sender, System.EventArgs e)
{
    GC.Collect();
}

Now when I banged on the site, the ‘Dispose’ break point is reached and the connection thread count is under control.

Having all the evidence in front of me, it is now clear as to what is going on. Without the call to ‘GC.Collect’, garbage collection occurs too infrequently. Ninject’s implementation of life cycle management based upon garbage collection may not be able to manage the lifecycle of critical resources. Maybe I was naive to not take responsibility for such resources. For instance, I could easily control the ‘end of life’ for such critical resources by using the following code:

void NinjectWebsiteApplication_EndRequest(object sender, System.EventArgs e)
{
    var uow = Kernel.Get<IUnitOfWork>();
    uow.Dispose();
}

Here, I am fetching the UnitOfWork instance and manually calling dispose. This code does not seem ‘too bad’, but feels a bit like I am short-circuiting the purpose of using an IOC container. I want the container to be responsible for the lifetime of the objects. It seemed to me that the ‘InRequestScope’ binding could be handled better by Ninject. Why not register for the ‘EndRequest’ event and release all the ‘InRequestScope’ bindings?

I posted this to the Ninject group and as usual got a very prompt reply. The Ninject group has some amazing people who never sleep working on the project. Remo suggested adding the following to the ‘NinjectHttpApplication’

public abstract class NinjectHttpApplication : HttpApplication, IHaveKernel
{
    private static IKernel _kernel;

    protected NinjectHttpApplication()
    {
        EndRequest += NinjectHttpApplication_EndRequest;
    }

    protected static void NinjectHttpApplication_EndRequest(object sender, System.EventArgs e)
    {
        _kernel.Components.Get<ICache>().Clear(HttpContext.Current);
    }

    /* Other code removed for brevity */
}

After adding this code, the ‘UnitOfWork’ instances are collected at the end of every request. Remo mentioned that this will be included in an upcoming release of the Ninject MVC extension.

In the meantime, be aware of how critical resources are being managed by your IOC container. I am not sure how other containers work. I am very happy with Ninject and have a number of projects using the library. As I mentioned the Ninject community is amazing.

If you are using an IOC container, how do you manage critical resources?

Posted in ninject | 2 Comments

Hello WordPress, Good Bye dasBlog

When I originally began blogging I selected the dasBlog blog engine for a number of reasons. The biggest reason was that it was a .NET blog engine and I was a .NET dev. I had reasoned that there would be instances where I would want to get up to my elbows in the .NET code of the blog engine and customize it. Well that turned out to not be true. I have pretty much been running the same version of dasBlog for the last couple of years. It has run really well and served up many blog posts of the years. This is not a dasBlog bashing post. In fact if you are looking for a .NET based blog engine, take a look at dasBlog.

image               image

I have been contemplating the move for quite some time. Recently, I upgraded my shared hosting account to a virtual private server. I have been transitioning my sites over to the new VPS and it just seemed like the right time to switch to WordPress. The big draw for me is that WordPress has a vibrant community and may just be the defacto blogging engine standard. There are thousands of plugins, widgets and themes already available for WordPress with many more coming soon.

The transition from dasBlog to WordPress has had its moments of “wow, this is going to be easy” to “on no, did I just lose a year’s worth of blogs?”. I will try to provide enough detail so that anyone else who may wish to go down this road has a bit smoother experience. Looking back, it really has not been such an ordeal. There are definitely some tricks and tips. If at all possible allow your self plenty of time to chase down and fix any little issues. It took me a weekend to perform the transition.

I did a search before I started and found a lot of help here. I pretty much followed this recipe, but ran into some issues.

Exporting From dasBlog

First off, get a backup of your dasBlog content. Bring it down to your local machine and archive it. You will be so glad that you have this content backed up in case you run into a situation where you need to back track. Here is a screen shot of my raw dasBlog content.

imageOne feature that I like about dasBlog is that all your content is stored on disk. I did all my publishing using Windows Live Writer. In the above screen shot you can see the “content” folder with the “YYYY-MM-DD.dayentry.xml” files containing the post text / meta and the “YYYY-MM-DD.dayfeedback.xml” files containing the post comments. A very simple system that works extremely well. The above screenshot also shows the Windows Live Writer content folders that contain images.

This data needs to be transformed into a format that WordPress can import. Following the advice of the above post I used the dasBlog to BlogML exporter tool. Below is a screenshot of the tool just before I clicked the “Export” button.

image It is a bit confusing that step 1 asks for the ‘DasBlog root folder’ but provides a ‘Save As’ dialog box when you click the ellipsis button. Just enter the path by hand. Step three is the name of the file that the tool will generate. Clicking the ‘Export’ button will start the conversion. If you are like me you will receive one of these dialogs:

image The information in the logs is important and you should copy and paste this into a file next to your exported BlogML data file. Here is a screenshot of my log data still in the form.

image Copy and past the data.

Repair The BlogML XML File

The first time I did the import into WordPress (yes, it took me more than one try…hopefully this info will save you the additional time), I imported this file thinking that I will just import the 6 posts with errors later by hand. WRONG! Don’t do this. It turns out that the XML of your BlogML file is potentially not valid markup. In my case there were missing closing tags and posts were nested inside other posts. The WordPress import seemed to go well on the surface, until I noticed that the first year or so of blog posts were missing. Take the time to fix this mark up. It will save you lots of time later.

I did not realize how bad the markup was hosed. I may have tried using different tools if I would have known. So pull out your best tools for this. If you have to you can use Internet Explorer to find the issues and then fix them in Notepad. Below is a screenshot of the issue you are searching for. At the top you see two collapsed posts and the one open one. Follow the open one down a bit. Just after the last ‘attachment’ what do you see….wait…on no that is a ‘post’ inside of a post.

image Guess what? If you import this, you loose all the posts that are below this one. I found about 6 or so (maybe related to my initial errors) of these that I needed to fix.

WordPress Import

I used the BlogML import plugin suggested in the original article. Just download and install in the “wp-admin/import” folder. Here is a screenshot of mine.

image

Before you use this plugin be sure to set up your URLs to be how you want for your blog. The plugin is going to provide you with a CSV file that will contain the information you need to setup 301 permanent redirects for your old URLs. This CSV file will be placed in your “wp-admin” file. Be sure the anonymous user (IUSR) in my case has write privilege to this folder for this operation.

Once you install, you will find this plugin in the “Tools” menu. Here is a screenshot of what it looks like on my install:

imageClicking the link results in the following dialog:

image

You will simple navigate to the BlogML data file you created earlier and click the “Upload file and import” button. I didn’t capture a screenshot of the activity after you do this (and no I am not going through this again to get that screenshot). You will see a page indicating each post that has been imported.

The generated CSV file is called “permalinkmap.csv” and as mentioned before will be created in your “wp-admin” folder. It has the following format:

OldPermalink,NewPermalink

http://blog.bobcravens.com/2009/10/03/PollingBasedMessageBusForASPNETWebServices.aspx,http://blog.bobcravens.com/2009/10/polling-based-message-bus-for-asp-net-web-services/

http://blog.bobcravens.com/2009/10/03/ProductReviewReSharper45.aspx,http://blog.bobcravens.com/2009/10/product-review-resharper-4-5/

...

As you can see the format is the old URL (notice the ASP.NET extension) followed by the new URL (using my selected URL style that I configured in WordPress).

Creating and Managing Redirects

As suggested in the original article, I used the WordPress Redirection plugin created by John Godley.

image

The original article also provide a nice breakdown on how to convert the “permalinkmap.csv” file into a format that can be imported into the Redirect plugin. Here is the expected format:

redirect 301 http://blog.bobcravens.com/2009/10/03/PollingBasedMessageBusForASPNETWebServices.aspx http://blog.bobcravens.com/2009/10/polling-based-message-bus-for-asp-net-web-services/
redirect 301 http://blog.bobcravens.com/2009/10/03/ProductReviewReSharper45.aspx http://blog.bobcravens.com/2009/10/product-review-resharper-4-5/
...

The three things to notice are:

  1. The header line has been removed.
  2. An extra column has been added with the value of “redirect 301”
  3. The columns are now space delimited.

Another  important thing is to rename your file. I think the Redirect plugin treats the data different if it has the CSV extension. The first time I imported (with the CSV extension) the routes were imported as a ‘pass through’ and not a ‘301 permanent redirect’. I renamed my file to ‘redirect.htaccess’.

If you expand the WordPress ‘Tools’ menu you should find the ‘Redirection’ option. At the top of the page you will see a menu with the ‘Options’ option. On the options page, you will find a section labeled ‘Import’.

image This option will allow you to navigate to and select your redirect file. Keep the ‘Import into’ option set to ‘Redirections’ and then click the ‘Upload’ button. You will then have a number of redirect definitions. Here is a screenshot of a few of mine:

imageNotice the ‘301’ and the ‘301 – Moved Permanently’ in the drop down list. This all works very well. There is even an indicator on the right hand size of the number of times this redirect has been issued. As also mentioned in the previous article, you should enter a number of regular expression based redirects. Here is a list for completeness:

/(?i)CommentView,guid,(.*)\.aspx             http://blog.bobcravens.com/CommentView.aspx?guid=$1
/(?i)default,month,(\d*)-(\d*)\.aspx         http://blog.bobcravens.com/$1/$2
/(?i)default.aspx                            http://blog.bobcravens.com/
/(?i)default,date,(\d*)-(\d*)-(\d*)\.aspx    http://blog.bobcravens.com/$1/$2/$3
/(?i)CategoryView,category,(.*)\.aspx        http://blog.bobcravens.com/category/$1/
/(?i)default\.aspx\?date=(\d*)-(\d*)-(\d*)   http://blog.bobcravens.com/$1/$2/$3
/ct.ashx\?id=(.*)\&url=(.*)                  http://blog.bobcravens.com/CommentView.aspx?guid=$1
/PermaLink,(.*)\.aspx                        http://blog.bobcravens.com/CommentView.aspx?guid=$1
/(?i)Trackback\.aspx\?guid=(.*)              http://blog.bobcravens.com/CommentView.aspx?guid=$1
/(?i)aggbug\.ashx\?id=(.*)                   http://blog.bobcravens.com/CommentView.aspx?guid=$1

Notice you will need to modify these slightly (please don’t redirect to ‘http://blog.bobcravens.com’). I entered these by hand in the form provided at the bottom of the Redirect plugin page.

image

Managing Broken Links

The next thing that I did was to install the Broken Link Checker WordPress plugin created by Janis Elsts.

image

This plugin will crawl your content (posts, comments…etc) looking for broken links. I had a lot of them! Mostly because early on I had dasBlog configured to think its home directory was ‘http://bobcravens.com/dasblogce’ instead of ‘http://blog.bobcravens.com’. This plugin found all the broken links and allows you to fix them with in-place editing. Another source of my broken links was that I wanted to move my Windows Live Writer content files to a new folder. This broke many image links. The Broken Links plugin allows you to easily find and repair those too.

Summary

So the transition was not necessarily without a hitch. After about 2 days I have managed to get through the process. I believe that most of my links have redirects in place. Another nice feature of the Redirects plugin is that it allows you to manage requests that end in a 404 page not found. This is great information for you to use to create a new 301 redirect. I am seeing a few of these and will work them as they come in.

I think that I can call the transition a success. I am enjoying the WordPress plugins tremendously. The plugins that I used to facilitate this transition alone have been amazing assets. If you are thinking of transitioning from dasBlog to WordPress, you should do it. Just save your self enough time and keep your backups in place.

Posted in Uncategorized | 3 Comments

Random Sparks Joins The Lounge

I really enjoy writing about and sharing my technical journeys. Recently, I have decided to upgrade my shared hosting account to a virtual dedicated server hosting account. This will allow my more freedom in the types of content I can publish. For instance, since I control the server I can publish ASP.NET content that requires full trust.

Unfortunately, this is a bit more expensive. To offset this cost a bit I looked around for a non-intrusive advertizing network. I really wanted something that was clean, wouldn’t detract from the main content and provided ads that targeted my audience.

After looking around a bit, I decided to accept an invitation to join The Lounge. The Lounge provides high quality advertising that targets Microsoft focused developers. So far, my experience with The Lounge has been great. I had a number of questions before joining that were kindly answered.

I appreciate that my readers enjoy my the content. The money earned from the ads will help cover the cost of hosting. Thanks to The Lounge for providing a professional service and thanks to my readers for visiting.

Posted in Uncategorized | Leave a comment

Location Based Service and Geo-Fencing

The concept of location based service is really very simple. Imagine you have a map. Pick a point of interest and draw a perimeter around the point. This perimeter is your geo-fence. In the map below the blue area represents is the geo-fence. Now imagine services or actions being taken based upon an object or person entering or leaving this perimeter. That is the concept of location based services.

image

The real value of location based services is when real-time or “quasi” real-time services can occur. This has not been a reality for most of us until recently. The miniaturization of GPS receivers has brought us a world where the devices we wear on a daily basis are GPS equipped.

GPS units will continue to shrink and we may someday see a “wearable” unit that can be embedded into clothing.

The basic form of location based services is shown below:

imageThe GPS enabled device posts location data to a Server. The Server then sends ‘data’ back to the device. The ‘data’ from the server has location aware content. This ‘data’ can be discounts at stores / restaurants in your vicinity, an alert indicating a friend is close by, or information about events happening in the area. These type of services that provide geo-centric data generally fall into the following categories:

  • Navigation – There are a number of applications that provide navigation. Some turn-by-turn. Others provide information on restaurants/stores in the vicinity. As the technology matures we are seeing more sophisticated navigational services. For example the server uses the latest traffic data to estimate the fastest route.
  • Informational – These services provide a geo-centric search engine. What events are happening in the area? What are the places of interest. Services can allow users to provide ratings for the events / places to provide a list of high quality things to do.
  • Tracking – GPS tracking services usually store the location and allow the data to be viewed using their software. This includes tracking your athletic activities and providing you with distance / timing information. This is already used in many industries to track mobile assets or to provide emergency services.
  • Advertising – As a store owner, what more targeted advertising than those people already near your store. Imagine as you walk by your favorite store, you receive a coupon that expires in the 1 hour. This may be how many location based services pay their bills. Many will provide a service in one of the other categories and sell location based advertising to pay their bills.
  • Social – Many location based services already allow you to network with others based upon your location. Some allow you to allow to accept friends and share your locations. Others create geo-location based games where you compete to become ‘mayor’ of the location.

The above services are typically what we see offered in today’s location based applications. I believe there is another wave services on the horizon. This next wave of services will combine geo-location with another feature (email, phone, billing…etc) to provide a new type of service. For example imagine…

  • A service that combined geo-location of your car with paying road tolls. No more stopping for tolls. You are billed as you drive.
  • A service that sends text messages alerting others you are leaving a zone (geo-fence). This could be handy for letting the family know that you are on the way home from work.
  • A reminder service could call your cell phone if you not on time leaving for an appointment. A friendly reminder to leave the work zone if the time is after 6 PM.
  • An service could automatically modify the environment (heat, lights…etc) of your house based upon your location.
  • A geo-centric scheduler for people who travel a lot could provide information to ease the pain of traveling. This could be configured to provide driving instructions and alerts based upon your current location. Additional options might be to always provide the nearest Starbucks location.

Another popular location based service will be to combine other sensors to enhance an experience. For example imagine…

  • There are already cameras that provide the ability to “geo-tag” a picture. This allows images to not only be  sorted based upon time but also upon location.
  • Athletic packages are becoming available that combine heart rate monitoring with location tracking to provide an additional level of detail about your activity.
  • A vehicle that measures gasoline levels/consumption and provides directions to the nearest gas stations (with pricing information) when the level is low.
  • Combining the real time video with location based services to provide an augmented reality. I have already seen some cool examples. This will only get better.

There is a concern over privacy and security. However, it doesn’t seem to deter the adoption of many current location based services. Many people either don’t care or don’t realize. I am on the side of “don’t care” as long as I am being provided a service and I can opt out if I no longer am interested. Security will be a big deal. For instance, I would love to track my kids but wouldn’t want anyone else to be able to see that information.

With the rapid adoption of GPS enabled mobile devices and the explosion of service based web applications, the next few years should provide some interesting location based services.

Posted in geolocation, mapping | 1 Comment