You must be logged in to view this resource.

Introduction

In this tutorial, we will build an application that analyzes a set of GPS points such as you might receive from a vehicle in the field. It determines the number of miles these points represent by two methods:

  • Using a distance meter.
  • Using a fitted route.

The resulting totals illustrate how you can achieve greater accuracy using a fitted route. In addition, the application uses details that the fitted route makes available to further analyze the vehicle's route, giving a breakdown by State, by road class, and by speed limit.

Prerequisites

This tutorial assumes familiarity with Microsoft Visual Studio. You will need a copy of Visual Studio 2005 or later and a moderately specified desktop computer. You will need a licensed copy of the GeoBase SDK. A 30-day free-of-charge trial may be downloaded from the GeoBase developer portal: http://geozone.geobase.info/. Two versions of the trial SDK are available, one loaded with US (West Coast) map data, the other version loaded with map data for UK and Ireland. The locations used in this tutorial assume US data.

This tutorial also assumes that you are already familiar with the basics of creating a GeoBase-specific Visual Studio Project and adding a map control to a GeoBase project.

Setting up

Open a new instance of Visual Studio and create a new Windows Forms application. Add geobase.net.dll as a reference.

In the code-behind, at the top of the Form1.cs source file, add the following directives:

using Telogis.GeoBase;
using Telogis.GeoBase.Routing;
using Telogis.GeoBase.Navigation;

Designing the form

Create a form to look similar to the one shown below:

Name and set properties on the controls as shown in the table below:

Control Name Properties
MapCtrl mapCtrl Zoom=120
NumericUpDown upDownMinutes Minimum=1
Button buttonGetPoints Text="Get Points"
Button buttonAnalyze Text="Analyze Route"
ListBox listBoxStats  

Initializing the form

Add the following declarations to the Form1 class for holding the GPS points to analyze and drawing them on the map:

Position[] points;
RendererList renderers = new RendererList();

Note that we store the GPS points as an array of Position structures. The Position structure, defined in the Telogis.GeoBase.Navigation namespace, is the way the IGps interface exposes incoming GPS points. 

 Update your Form1 constructor so that it looks like the following:

public Form1() {
    InitializeComponent();
    mapCtrl.Renderer = renderers;
}

This lets us use the RendererList we declared to mark the GPS points on the map.

Generating the points

Before we can analyze them, we need to have an array of GPS points. We will get this by adding a handler for the Click event of buttonGetPoints. In a real application, the points would most likely come from a class that implements the IGps interface. However, for the purposes of this tutorial, we will generate our own points by creating a route and then using the information from that route to construct a realistic set of points. At the same time we generate the points, we will add a PushPin to the map at the corresponding location so that you can see the points we are analyzing.

Add the following handler for the buttonGetPoints Click event:

private void buttonGetPoints_Click(object sender, EventArgs e) {
    LatLon start = new LatLon(35.841620, -114.571620);
    LatLon end = new LatLon(35.433020, -115.705050);
    BoundingBox bounds = new BoundingBox();
 
    renderers.Clear();
    List<position> pointList = new List<position>();

    DateTime startTime = DateTime.Now; 
       
    // add a point for the starting location
    pointList.Add(NewPoint(start, startTime, -1, 0));
    bounds.Add(start);

    // now generate additional points along the route
    Route route = new Route(new RouteStop(start), new RouteStop(end));
    Directions dirs = route.GetDirections();
    TimeSpan elapsed = TimeSpan.Zero;
    foreach (DirectionLink link in dirs.GetDirectionLinks()) {
        if (link.ExpectedEndTime.TotalMinutes - elapsed.TotalMinutes > (int)upDownMinutes.Value) {

            elapsed = link.ExpectedEndTime;
            pointList.Add(NewPoint(
                link.LastPoint,
                startTime + elapsed,
                MathUtil.GetHeading(link.FirstPoint, link.LastPoint),
                MathUtil.ConvertUnits((double)link.Flags.ImpliedSpeedLimit,
                    link.Flags.SPEED_UNITS == SpeedUnit.Unknown 
                        ? SpeedUnit.MilesPerHour 
                        : link.Flags.SPEED_UNITS,
                    SpeedUnit.Knots)));
            bounds.Add(link.LastPoint);
        }
    }
    points = pointList.ToArray<position>();
    mapCtrl.ZoomToBoundingBox(bounds, 15);
    mapCtrl.Invalidate();
}
// generate a Position and add a pin to the map to show its location
private Position NewPoint(LatLon location, DateTime time, double heading, double speed) {
    Position pos = new Position();
    pos.location = location;
    pos.time = time;
    pos.heading = heading;
    pos.speed = speed;

    PushPin pin = new PushPin(location);
    renderers.Add(pin);
    return pos;
}

Each point that we add (other than the first) is based on information in a DirectionLink object. These objects are generated as part of the Directions for a route. Each DirectionLink represents a portion of a route that goes between two choice points (such as intersections). The DirectionLink objects form a linked list, where the last point in one DirectionLink is the same as the first point in the next. In addition to the location information, the DirectionLink objects supply useful information about the corresponding portion of a route, which we make use of to determine the likely time the vehicle reaches the endpoint, a reasonable heading for the vehicle, and a reasonable speed for the vehicle. Note that we represent the speed in Knots. This is for convenience, because when we generate a route to analyze these points, the route object expects to get speed values in knots.

Run the application and click the Get Points button. Note that the generated points appear on the map, marked by push pins.

If you like, you can change the frequency of points and generate a different set to see the effect.

Calculating distance driven

Now that we have a set of points, we are ready to start analyzing the route. To start with, let's calculate the total distance driven. We will do this using a distance meter and using a fitted route.

The distance meter has the advantage that it is quick and easy. All you need to do is add the locations to the distance meter and then read off the total distance. However, the distance meter calculates distance by adding up the straight line point-to-point distances between each of the positions we pass it. Real roads are unlikely to follow those straight lines exactly, so the resulting value will underestimate the actual distance driven.

The fitted route takes a little longer to calculate because it must deduce the most likely route that the vehicle used, based on the time, speed, and heading of each point in addition to the location. However, the result is far more accurate because it calculates distance based on the actual road length rather than a simple point-to-point distance.

Add the following handler to the Click event of buttonAnalyze:

private void buttonAnalyze_Click(object sender, EventArgs e) {
    listBoxStats.Items.Clear();
    if (points.Length == 0) {
       return; // nothing to analyze
    }
    // create a distance meter to compute point to point distance
    DistanceMeter dm = new DistanceMeter();

    // create a route to compute distance based on fitted route
    Route history = new Route();

    // add each point to both the distance meter and the route
    DateTime lastTime = points[0].time;
    for (int i = 0; i < points.Length; i++) {
        Position pos = points[i];
        // add the point to the distance meter
        dm.Add(pos.location);

        // add the point to the history route
        RouteStop stop = new RouteStop(pos.location);
        stop.TimeSincePreviousStop = (int)(pos.time - lastTime).TotalSeconds;
        lastTime = pos.time;
        stop.Heading = (float)pos.heading;
        stop.Speed = (float)pos.speed;
        history.AddStopAtEnd(stop);
    }
    listBoxStats.Items.Add("Distance Meter: " + Math.Round(dm.Distance(DistanceUnit.MILES), 2).ToString() + " miles");

    Directions dirs = history.GetRouteHighlight();
    listBoxStats.Items.Add("Fitted Route: " + Math.Round(dirs.GetTotalDistance(DistanceUnit.MILES), 2).ToString() + " miles");
}

For each position, this method adds the location to the distance meter and generates a route stop to reflect the addition information in the position, which it adds to the route.  To get the distance from the distance meter, we call its Distance method. To get a more accurate distance from the route object, we call GetRouteHighlight(), which figures out the most likely route the vehicle took based on the stops we supplied, and then call GetTotalDistance to calculate the total distance along the roads of the most likely route.

Run the application, click the Get Points button to generate some data, and then click the Analyze Route button.

 

Note that the distance meter produces a smaller result than the fitted route. Try changing the frequency of points and analyzing the new set of points. Note that the distance estimate based on a fitted route is more stable than the value based on the distance meter. The less frequent the points, the more the distance meter underestimates the total distance.

Further Analysis

We already saw, when generating our simulated GPS points, that a Directions object can provide a linked list of DirectionLink objects to represent each segment of a route. When calculating distance from a fitted route, we obtained a Directions object, which means we have all the information supplied by these DirectionLink objects at hand. This makes it very easy to further analyze the route to break down the miles by categories such as which state the road was in or what was the road class.

Let's enhance our application to further analyze the route to provide a breakdown of mileage by

  • State.
  • Road Class.
  • Speed Limit.

To determine the state, we do not need to do any reverse geocoding, because the DirectionLink conveniently has an Address property. The state is surfaced as the Region property of the Address.

To determine the road class, we use the Flags property of the DirectionLink. Flags holds a wealth of information about the road segment that the DirectionLink encodes, including many types of road restrictions, the number of lanes on the road segment, and so on. To keep things simple, we will use the FUNC_CLASS property.  FUNC_CLASS categorizes roads into the following five road classes:

  1. High volume, maximum speed roads with very few, if any, speed changes. Typically, class 1 roads are limited access.
  2. High volume, high speed roads with very few, if any, speed changes. Typically, class 2 roads channel traffic to and from class 1 roads.
  3. High volume, more moderate speed roads that interconnect Class 2 roads.
  4. High volume, moderate speed roads that traverse neighborhoods.
  5. Other roads.

To determine the speed limit, we again use the Flags property, this time using the ImpliedSpeedLimit property. (You may have noticed that we already used this one when generating our simulated GPS points). ImpliedSpeedLimit uses actual speed limit data if it is known. Otherwise, it estimates the speed limit based on the other properties of the road segment.

Add the following code to the end of the buttonAnalyze_Click event handler:

// now show the breakdown by roadclass, state, speedlimit
Dictionary<uint, double> milesbyClass = new Dictionary<uint, double>();
Dictionary<string, double> milesbyState = new Dictionary<string, double>();
Dictionary<int, double> milesbySL = new Dictionary<int, double>();
foreach (DirectionLink link in dirs.GetDirectionLinks()) {
    double linkMiles = link.FirstPoint.DistanceTo(link.LastPoint, DistanceUnit.MILES);
    // check state
    string state = link.Address.Region;
    double miles = milesbyState.ContainsKey(state) ? milesbyState[state] : 0.0;
    miles += linkMiles;
    milesbyState[state] = miles;

    // check roadclass
    uint RoadClass = link.Flags.FUNC_CLASS;
    miles = milesbyClass.ContainsKey(RoadClass) ? milesbyClass[RoadClass] : 0.0;
    miles += linkMiles;
    milesbyClass[RoadClass] = miles;

    // check speed limit
    int speedLimit = link.Flags.ImpliedSpeedLimit;
    if (link.Flags.SPEED_UNITS != SpeedUnit.Unknown && link.Flags.SPEED_UNITS != SpeedUnit.MilesPerHour) {
        speedLimit = (int)Math.Round(MathUtil.ConvertUnits((double)speedLimit, link.Flags.SPEED_UNITS, SpeedUnit.MilesPerHour));
    }
    miles = milesbySL.ContainsKey(speedLimit) ? milesbySL[speedLimit] : 0.0;
    miles += linkMiles;
    milesbySL[speedLimit] = miles;
}
listBoxStats.Items.Add("====== Miles by State ========");
foreach (string key in milesbyState.Keys) {
    double miles = milesbyState[key];
    listBoxStats.Items.Add(key.ToString() + " miles: " + Math.Round(miles, 2).ToString());
}
listBoxStats.Items.Add("====== Miles by RoadClass ========");
foreach (uint key in milesbyClass.Keys) {
    double miles = milesbyClass[key];
    listBoxStats.Items.Add("Road Class " + key.ToString() + " miles: " + Math.Round(miles, 2).ToString());
}
listBoxStats.Items.Add("====== Miles by Speed Limit ========");
foreach (int key in milesbySL.Keys) {
    double miles = milesbySL[key];
    listBoxStats.Items.Add("Miles at " + key.ToString() + ": " + Math.Round(miles, 2).ToString());
}

This totals the miles in each DirectionLink, grouping the data by state, road class, or speed limit. Note that these distance measurements are not as accurate as the total distance for the route, because we are using point-to-point distances for the endpoints of each DirectionLink object. However, the results are still more accurate than the distances supplied by the distance meter, because we have more direction links than GPS points.

Conclusion

In this tutorial, we have seen how to analyze a set of GPS points by determining the most likely route that the vehicle took and then analyzing the segments that make up that route. This approach lets us take advantage of the wealth of information about each segment of the route that can be found in the DirectionLink object, so that we can easily get subtotals for state, road class, speed limit, and so on.

Published, Aug 1st 2010, 21:11

Tagged under: dotnet geobase routing