Introduction

When you define a route, you can estimate the distance the vehicle that follows that route will cover and, if you know the time of departure for the vehicle, you can get an estimated time of arrival (ETA). However, when a vehicle actually follows that route, the initial estimates may prove inaccurate: vehicles can slow down due to traffic, they can make unexpected stops, or make wrong turns.

This tutorial shows how to use incoming real-time data to update information about a route to take into account the vehicle's actual movement. We will build an application that uses two Telogis GeoBase features which help us take advantage of incoming GPS signals to refine measurements of the time and distance a vehicle is travelling:

  • The GetRouteHighlight method of the Route object analyzes the GPS points that have come in to deduce the most likely route that a vehicle took, compensating for minor drift in GPS signal by using information about vehicle speed and heading in addition to map data.
  • The Navigator class manages the planned route from the current position (based on a GPS point) to a final destination, updating the directions only when necessary (when a GPS signal indicates that the vehicle has strayed from the originally planned route). It also provides estimates of the remaining time to arrival that take into account a vehicle's current speed and heading.

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 also need a licensed copy of the Telogis 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 the UK. 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.

Design the Form

Create a form to look similar to that shown below:

Name and set the controls as shown in the table below. (Note that only the label controls without bold fonts are included in the table -- these are the only ones referred to in code).

Control Name Properties
MapCtrl mapCtrl  
Label labelStartTime Text = "0:00:00"
Label labelETA Text = "0:00:00"
Label labelDistanceGone Text = "0 miles"
Label labelTimeSoFar Text = "0:00"
Label labelDistanceToGo Text="? miles"
Label labelTimeToGo Text = "0:00"
Label labelDistancePercent Text="0%"
Label labelTimePercent Text="0%"

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

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

Initializing the Form

When the form first loads, we will define a route and display it on the map. We will also create a second route object that will represent the emerging historic route based on our incoming GPS data. Before we do that, add the declarations to Form1:

// ActualRoute will perform historic route fitting 
// for an accurate measure of where we've been 
private Route ActualRoute;

// the statistics to display 
DateTime eta; 
TimeSpan timeToGo; 
TimeSpan timeGone = TimeSpan.Zero; 
double distanceToGo; 
double distanceGone = 0.0;
 
LatLon start = new LatLon(33.58127, -117.72704); 
LatLon end = new LatLon(33.589120, -117.631568); 

RendererList renderList = new RendererList();

In the Form1 constructor, we will set up the map control and create the route object for holding incoming data to be used for historic route fitting. Modify your Form1 constructor so that it looks like the following:

public Form1() {
    InitializeComponent();
    // initialize the map
    mapCtrl.Renderer = renderList;
    BoundingBox bb = new BoundingBox();
    bb.Add(start);
    bb.Add(end);
    mapCtrl.ZoomToBoundingBox(bb, 50);

   // initialize the actual route with the start location
   ActualRoute = new Route();
   ActualRoute.AddStop(new RouteStop(start));
} 

Next, we create a Form1_Load event handler to update the UI, adding the planned route to the map and putting in our initial estimates of the time and distance to go, percent complete, and ETA.

private void Form1_Load(object sender, EventArgs e) {
 
  // calculate the planned route, update time and distance to go
  Route PlannedRoute = new Route(new RouteStop(start), new RouteStop(end));
  Directions dirs = PlannedRoute.GetDirections();
  timeToGo = dirs.GetTotalTime();
  distanceToGo = dirs.GetTotalDistance(DistanceUnit.MILES);

  renderList.Add(dirs);
  eta = DateTime.Now + timeToGo;
  UpdateUI();
} 

This code uses the GetTotalTime and GetTotalDistance methods of the Directions that the route generates in order to supply initial estimates of the time and distance for the route. Later, when we have incoming GPS data, we will update these values.

The last line of the event handler we just added is a call to the UpdateUI method. Let's add that method now:

private void UpdateUI() {
   double percent;

   mapCtrl.Invalidate(); // force redraw
   labelETA.Text = eta.ToShortTimeString();

   labelTimeToGo.Text = timeToGo.ToString("c");
   labelTimeSoFar.Text = timeGone.ToString("c");
   percent = timeGone.TotalSeconds * 100 / (timeToGo.TotalSeconds + timeGone.TotalSeconds);
   labelTimePercent.Text = System.Math.Round(percent, 1).ToString() + "%";

   labelDistanceToGo.Text = System.Math.Round(distanceToGo, 2).ToString() + " miles";
   labelDistanceGone.Text = System.Math.Round(distanceGone, 2).ToString() + " miles";
   percent = distanceGone * 100 / (distanceGone + distanceToGo);
   labelDistancePercent.Text = System.Math.Round(percent, 1).ToString() + "%";
}

Try running the application. You should see a screen like the following:

You can see the proposed route, and all of the statistics at the bottom of the form (except Start Time) have their initial values.

Adding a Navigator

Although we haven't really used it yet, our application already has a Route object for handling the data points that have already come in.  You may have noticed that the Route object for the planned route, however, is only a local variable. That is because, as GPS data comes in, it will be more efficient to use a Navigator object to manage the planned route.

The Navigator object maintains a proposed route in conjunction with incoming vehicle data, updating the proposed route when necessary (if the vehicle strays from the proposed route). It also can provide us with accurate estimates of the remaining time until the vehicle reaches its destination, using not just the vehicle's current location, but also its speed and heading.

Add a declaration for the Navigator object to Form1:

// nav manages the proposed route to our destination
private Navigator nav;

// Time when GPS simulator starts
DateTime startTime; 

Notice that we added another declaration as well. This is the time when the navigator gets its first point.

Add the following lines to the end of the Form1_Load event handler:

nav = GenerateNavigator(dirs);
// set the official start time based on the GPS simulator
labelStartTime.Text = startTime.ToShortTimeString();      

This calls GenerateNavigator (defined below) to create a navigator for the proposed directions we already generated for obtaining our initial statistics. As a side effect, GenerateSimulator also sets startTime to the time when the navigator gets its first point, which we can use to set the Text of labelStartTime.

Add the GenerateNavigator method:

// Generate a Navigator 
// and hook up the event handler for processing points
private Navigator GenerateNavigator(Directions dirs) {
    // use a custom simulator so that we don't follow planned route exactly
    MySimulator sim = new MySimulator(dirs);
    startTime = sim.Time;
    sim.UpdateInterval = 2;

    // create a navigator to get points from the simulator
    nav = new Navigator(sim);
    // set the destination
    nav.Destination = new RouteStop(end);
    // Hook up event handlers
    sim.Update += NewPoint;
    nav.Arrived += Arrive;
          
    return nav;
}

Note that the constructor for the Navigator class takes an IGps as an argument. This is the interface that the navigator uses to get incoming position data. We are passing in a Gps simulator to supply points. We could use the GpsSimulator class, but that class follows the route exactly, which would mean that we would end up with no deviation from our initial estimates of the eta. To prevent this, we will define our own simulator class that tweaks the Gps points slightly, adding a bit of randomness. This will also let us see how the interpretation of past data points can change with the addition of more data. For example, if a point drifts off a bit, it may look like the vehicle is moving off the planned route, while subsequent points make it clear that it is more likely that the vehicle was continuing in a straight line.

public class MySimulator : GpsSimulator {

  // constructor is just the base constructor with a time
  // multiplier of 2 to speed up the simulation
  public MySimulator(Directions dir) : base (dir, 2) {
  }

  // Randomly tweak the location a bit.
  public override LatLon Location {
      get {
          Random rand = new Random(DateTime.Now.Millisecond);
          LatLon actualloc = base.Location;
          actualloc.Lat += rand.Next(-100, 100) * 0.000005;
          actualloc.Lon += rand.Next(-100, 100) * 0.000005;
          return actualloc;
       }
   }

   // use our tweaked location for the position
   public override Position Position {
       get {
           Position pos = base.Position;
           pos.location = Location;
           return pos;
       }
   }

}

At the end of GenerateNavigator, we added two event handlers -- one to the GPS simulator and one to the navigator. These are defined in the next section.

Responding to Incoming Points

The Update handler for the GPS simulator is required so that when the GPS simulator generates a new point, we can tell the Navigator that it needs to update itself by reading from its GPS device. This is an important step: the Navigator does not automatically update itself.

 private void NewPoint(object sender, EventArgs e) {
    // pass the point to the navigator
    nav.AddPoint();
    // recalculate the time and distance estimates
    UpdateRoutes();
    // because the navigation manager uses a separate thread,
    // we must use Invoke to update the UI on the Windows UI thread
    mapCtrl.Invoke(new Action(delegate() { UpdateUI(); }));
}

In this event handler, the call to AddPoint tells the Navigator to update its position data. After that, we call UpdateRoutes (defined below) to update all of our route information based on the new point. Finally, we call UpdateUI to update the form so that it displays the new data. Note that the call to UpdateUI  uses the Invoke method. That is because the GPS simulator generates its Update event on a non-UI thread, and UpdateUI must execute on the main windows thread.

Before we get to the UpdateRoutes method, let's quickly add the Arrive method we hooked up to the Navigator's Arrived event. This just shuts down the application when the vehicle reaches its destination.

private void Arrive(object sender, EventArgs e) {
    nav.Gps.PowerDown();
    Application.Exit();
}

The core of this application is the UpdateRoutes() method. This method uses the historic route information to update the information about the past (distanceGone and timeGone), and uses the Navigator to update the estimates of the remaining route (timeToGo, distanceToGo, and eta). For historic data, we need to know the time that has elapsed since the last point we added to the Route of actual points. In order to keep track of these times, add another declaration to Form1:

// Time of last point
DateTime prevTime;  

Now add the following code to implement UpdateRoutes:

private void UpdateRoutes() {
    Position curPosition = nav.Position;           
    DateTime curTime = curPosition.time;
    LatLon curLocation = curPosition.location;
 
    if (!curLocation.Equals(start)) {
        // calculate our actual time and distance gone
        RouteStop actualStop = new RouteStop(curLocation);
        actualStop.TimeSincePreviousStop = (int)(curTime - prevTime).TotalSeconds;
        actualStop.Heading = (float)nav.Heading;
        actualStop.Speed = (float) curPosition.speed;
        ActualRoute.AddStopAtEnd(actualStop);
        Directions actual = ActualRoute.GetRouteHighlight(); // fit history
        distanceGone = actual.GetTotalDistance(DistanceUnit.MILES);
        timeGone = curTime - startTime;
        actual.RenderColor = Color.Crimson;
 
        // the navigator estimate of time remaining takes into account 
        // the current speed and heading, 
        // so it is more accurate than using the directions object
        timeToGo = nav.TotalTimeRemaining;
        distanceToGo = nav.GetTotalDistanceRemaining(DistanceUnit.MILES);
        eta = curTime + timeToGo;

        // update the renderList for drawing the new routes
        renderList.Clear();
        renderList.Add(nav.Directions);  
        renderList.Add(actual);
    }
    prevTime = curTime;
}

UpdateRoutes first pulls information off the current position of the navigator to add to the historic route we are building. It then generates a new RouteStop based on the latest position, including a calculation of the time since the last stop. We then get a the directions for the past route by calling GetRouteHighlight(), which uses the locations, times, headings, and speed of the previous route stops to deduce the most likely route the vehicle took. GetRouteHighlight requires, as a minimum, that past route stops include a location and TimeSincePreviousStop. The Heading and Speed are optional, but add to the accuracy of resulting Directions object.  We can then set distanceGone and timeGone to our most accurate estimates of the vehicle's actual route.

For the predicted time and distance, we do not use the navigator's directions object; the TotalTimeRemaining property and GetTotalDistanceRemaining method provide more accurate estimates.

Run the application.

As the GPS simulator generates points, the UI updates to give more accurate time and distance measurements (and estimates). Note that when the drifty GPS simulator moves off route, the next point that comes in often corrects much of the error in the historic portion of the route.

Identifying Route Deviation

In the application we just built, the simulator caused our vehicle to occasionally stray from the planned route. Most likely, the only indication of this straying was in the actual route, where the red line drifts off the planned route and the "distance travelled so far" value is larger than expected.

Because GPS devices are not completely accurate, the navigator includes a certain tolerance from deviation from a planned route before it decides that a vehicle is off course. You can configure how much deviation the navigator will tolerate before recalculating the route using the following property and method:

  • The DeviationGraceTime property specifies how much time the GPS signals can be off route before the navigator concludes that the vehicle is, in fact, off route.
  • The SetDeviationThreshold method lets you specify how far the position can stray from the planned route without a position being considered off route.

 When the incoming signal is farther than the deviation threshold for longer than the grace time, the navigator decides that the vehicle is off course. At that point, it recalculates the planned route. It also raises an event so that your application can generate other responses.

We will modify our application to use this event to display a message box when the vehicle is too far off course. Add the following event handler to your code:

private void OffCourse(object sender, NavigationEvent desc) {
   MessageBox.Show("Vehicle is off course! It should be on " + desc.TargetStreet);
}

Modify your GenerateNavigator method to hook up this event. We will also set the deviation threshold to be quite sensitive, so that the navigator will be more likely to detect route deviations:

private Navigator GenerateNavigator(Directions dirs) {
    // use a custom simulator so that we don't follow planned route exactly
    MySimulator sim = new MySimulator(dirs);
    startTime = sim.Time;
    sim.UpdateInterval = 2;

    // create a navigator to get points from the simulator
    nav = new Navigator(sim);
    // set the destination
    nav.Destination = new RouteStop(end);
    // Hook up event handlers
    sim.Update += NewPoint;
    nav.Arrived += Arrive;

    nav.SetDeviationThreshold(DistanceUnit.FEET, 100, 100);
    nav.OffCourse += OffCourse;
            
    return nav;
}

Run your application. When the navigator detects that the vehicle is off course, a message box appears:

Conclusion

In this tutorial, we have seen how to revise our initial time and distance estimates for a route by taking advantage of incoming GPS data.  We have seen how the GetRouteHighlight method of the Route object updates our estimates of where we have been, refining its interpretation of past points as additional GPS points come in.

We have also seen how to use the Navigator object to efficiently determine how much of a route remains to be travelled, letting it detect when a vehicle goes off course and recalculate the remaining directions only when necessary.

Published, Jul 8th 2016, 00:53

Tagged under: dotnet geobase routing navigation