GPS Programming Tips for Windows Mobile - Part 3

- GPS Signal Quality

- Error Reduction

- Avoid problems with localizations

- Data Layer: do NOT use XML files...

- Hints about showing itinerary with GPS Data independently on current device resolution\orientation

(- links to this project on Codeplex and to webcast Creating Location-Aware Applications for Windows Mobile Devices (Level 300) )

[Part 1 and 2 are available here and here.]

You may start from the NMEA basic parser sample provided by GeoFrameworks through their Mastering GPS Programming. When GPS signal quality is low, the GPS sends some data anyway [100Bytes - 300Bytes], so checking if you're reading 0 Bytes is not a good mean for signal quality. The values you can monitor within the NMEA sentences are:

  • HDOP = Words[8] of the GPGGA
  • QOS = Words[5] of the GPGGA
  • Satellites in view = Words[6] of the GPGGA

Then, there may be various ways you can reduce the error, which is there even if the signal quality is good. A quite straightforward one is to calculate the average of X positions received in a timeframe (imagine a buffer filled with NMEA sentences retrieved from the antenna). However this is dependent on the average speed and sincerely for the position I would not stress on finding an average. This is not true for example for speed values coming from the antenna, because their range may vary a bit even if you're running at the same speed (and I remembered I've read somewhere in the SportsDo website that their program uses a NASA's algorythm for speed values! - this was out of scope in my case... Hot [I found it (see "Data Correction") - it says that the algorithm is for altitude, but I'd assume it's the same for speed as well])

On top of this, consider extracting the GPS location from GPRMC, GPGGA and GPGLL at the same time. Note that not all the GPS Antenna rely on the same NMEA sentences, and not all have the same "sentence-pattern" - for example Sirfstar doesn't have GPGLL, and its pattern is:

 5x 
   $GPRMC - Recommended Minimum specific GPS/TRANSIT data
   $GPVTG - Track Made Good and Ground Speed
   $GPGGA - Global Positioning System fix data
   $GPGSA - GPS DOP and Satellites Active
3x
   $GPGSV - Satellites in View

In my case I was simply interested on the following and it sufficed:

  • GPRMC for Lat\Long and Speed
  • GPGGA for Lat\Long and Altitude

So, on top of the NMEA Parser, I considered an "intermediate" layer responsible for notifying upper UI with location changes. Regarding this, remember that if you have to parse Double values, then you must take in account the CurrentCulture. The NMEA Interpreter sample provided by GeoFrameworks simply set the current culture to en-US, and I basically did the same for all the UIs and intermediate layers: this avoids problems with different cultures and also provides a simple way to store data in the database consistently. Also, it allows you to be able to move the database from a device to another, or even to a PC if you want (SportsDo, for example, allows you to upload the data of the activities to their server).

Regarding the database: for a very initial draft of the application I had used the old way, i.e. store the data into a XML file. I chose so also in order not to ask my peers to install additional software's CABs (SQL Compact 3.5 or at least 3.1, which consist of 3 CABs) on low-resources old devices. The code was working fine, but after some "activities" we realized that the file was growing very fast, and writing was slower after each saved activity... we've even got OutOfMemoryExceptions sometimes! (see my previous post about NETCF Memory Leaks here) So, lesson learned: use SQL Compact, even if it'll require additional CABs when installing the application. Nerd

So, to recap, imagine:

  • a "physical" layer that retrieves data from a Bluetooth antenna (and an intermediate layer that handles BT signal loss)
  • a NMEA Interpreter and its intermediate layer that handles error reduction, informs upper layers
  • a data Intermediate layer listening on notifications from the NMEA one and storing data

So far we're running (or going bycicle, or rafting, whatsoever)... at the end of the activity we want to see data and show to friends to let them see how cool our Windows Mobile device is... Tongue out Well, playing with Graphics can be straightforward or painful depending on your goals. At the end of the day we're talking about a matrix of points representing the pairs latitude\longitude, speed\time, distance\time, altitude\time, etc... that you want to display, hopefully independently on device resolution and orientation. Adapting the application to the actual device capabilities can be done in many ways (and many others before me posted about that), see for example:

Remember that Graphic objects internally held a reference to NATIVE resources, therefore you must explicitly call .Dispose or use the "using" directive, which automatically call .Dispose when finished with the object for you. A possible draft-code is the following: even if it does what it's meant to do, I'm sure there are better ways... (for example, it may show info about speed during the activity - as SportsDo does, and technically-speaking it doesn't use double-buffering, nor even SuspendLayout\ResumeLayout, etc., etc., etc...) I'm pasting here as it might give some ideas anyway:

 /// <summary>
/// Show data details: takes a datatable (it could have been a hashtable or a dictionary), and depending on category, calculates ranges for X and Y
/// positions and creates a Points[] structure. Then use this.CreateGraphics to draw lines and polygons
/// </summary>
/// <param name="category">data category to show</param>
/// <param name="table">table containing data</param>
internal void Show(string category, DataTable table)
{
    Cursor.Current = Cursors.WaitCursor;

    //this is done at the beginning so that this.CreateGraphics can work
    this.Show();
    //let's make sure the Message Pump is not blocked
    Application.DoEvents();

    //table has 2 columns
    //when showing speed, distance, altitude, etc over time
    //Column1 is where relevant data is saved
    //Column2 is where time data is saved
    //when showing path, Column1 is the latitude and Column2 the longitude
    int Column1 = 0; 
    int Column2 = 0; 

    //when showing speed, distance, altitude a string will be drawn with max value
    string unit = string.Empty;
    
    //boolean values set depending on data to be shown
    //bool bSpeed = false;
    //bool bDistance = false;
    //bool bAltitude = false;
    bool bPosition = false;

    //depending on the category to "show", previous variables may assume different values
    #region switch (category)
    switch (category)
    {
        case "speed":
            Column1 = 1;
            Column2 = 0;
            unit = "kmph";
            //bSpeed = true;
            //bDistance = false;
            //bAltitude = false;
            bPosition = false;
            break;
        case "distance":
            Column1 = 1;
            Column2 = 0;
            unit = "km";
            //bSpeed = false;
            //bDistance = true;
            //bAltitude = false;
            bPosition = false;
            break;
        case "altitude":
            Column1 = 1;
            Column2 = 0;
            unit = "m";
            //bSpeed = false;
            //bDistance = false;
            //bAltitude = true;
            bPosition = false;
            break;
        case "position":
            Column1 = 0;
            Column2 = 1;
            //bSpeed = false;
            //bDistance = false;
            //bAltitude = false;
            bPosition = true;
            break;
        //default:
        //    //...
        //    break;
    }
    #endregion

    #region check if data was saved
    if (table.Rows.Count == 0) //no data has been saved
    {
        using (Graphics g = this.CreateGraphics())
        {
            Font f = new Font(FontFamily.GenericSansSerif, 16.0f, FontStyle.Bold);
            SolidBrush b = new SolidBrush(Color.Black);
            g.DrawString("NO DATA", f, b, 0.0f, 0.0f);
            b.Dispose();
            f.Dispose();
        }
        return;
    }
    #endregion

    //to maintain the code independent on the orientation of the device
    int ShortestSideOfTheDevice = Math.Min(this.ClientSize.Width, this.ClientSize.Height);

    //array to store points to be drawn
    Point[] points;

    //min,max, range values for latitude and longitude
    double minX, maxX, rangeX, minY, maxY, rangeY;

    #region fill points[] and calculate minX, maxX, rangeX, minY, maxY, rangeY when bPosition = false
    if (!bPosition)
    {
        //assume last row contains the total elapsed seconds
        maxX = double.Parse(table.Rows[count - 1][Column2].ToString(), frmMain.Instance.DataCultureInfo); 
        minX = 0;
        rangeX = maxX - minX;

        maxY = FindMax(table, Column1);
        minY = FindMin(table, Column1);
        rangeY = maxY - minY;

        //initialize Point array
        points = new Point[table.Rows.Count + 2];
        //adding 2 points [0, Height] and [Width, Height] because we want a polygon
        //first point
        points[0].X = 0;
        points[0].Y = this.ClientSize.Height;
        //last point
        points[table.Rows.Count + 1].X = this.ClientSize.Width;
        points[table.Rows.Count + 1].Y = this.ClientSize.Height;

        //fill Point array
        int i = 1;
        double percentX, percentY;
        foreach (DataRow row in table.Rows)
        {
            //handle range = 0, otherwise possible DivideByZeroException
            if (rangeX == 0.0f)
                percentX = 100.0f;
            else
                percentX = (double.Parse(row[Column2].ToString(), frmMain.Instance.DataCultureInfo) - minX) / rangeX;

            //handle range = 0, otherwise possible DivideByZeroException
            if (rangeY == 0.0f)
                percentY = 100.0f;
            else
                percentY = (double.Parse(row[Column1].ToString(), frmMain.Instance.DataCultureInfo) - minY) / rangeY;

            //discard the point if NaN
            if (!(double.IsNaN(percentX) || double.IsNaN(percentY)))
            {
                points[i].X = Convert.ToInt32(this.ClientSize.Width * percentX);
                points[i].Y = this.ClientSize.Height - Convert.ToInt32(this.ClientSize.Height * percentY);
                ++i;
            }
        }
    }
    #endregion

    #region fill points[] and calculate minX, maxX, rangeX, minY, maxY, rangeY when bPosition = true
    else //bPosition = true
    {
        //find Min\Max for latitude\longitude so that we can scale the path
        //note that coordinates may be negative, when represented by doubles
        //this applies to minX, minY, maxX, maxY as well
        maxX = FindMax(table, Column2);
        minX = FindMin(table, Column2);
        rangeX = maxX - minX;

        maxY = FindMax(table, Column1);
        minY = FindMin(table, Column1);
        rangeY = maxY - minY;

        //initialize Point array
        points = new Point[table.Rows.Count];

        //in order to put the "image" at the center:
        //1. calculate current center in terms of latitude\longitude
        double centerX = minX + rangeX / 2;
        double centerY = minY + rangeY / 2;

        //2. calculate position in pixel of the current center
        double percentCenterX = (centerX - minX) / Math.Max(rangeX, rangeY);
        double percentCenterY = (centerY - minY) / Math.Max(rangeX, rangeY);
        double PixelCenterX = Convert.ToInt32(ShortestSideOfTheDevice * percentCenterX);
        double PixelCenterY = ShortestSideOfTheDevice - Convert.ToInt32(ShortestSideOfTheDevice * percentCenterY);

        //3. shift to center screen
        double PixelToShiftX = (ShortestSideOfTheDevice / 2) - PixelCenterX;
        double PixelToShiftY = (ShortestSideOfTheDevice / 2) - PixelCenterY;

        //now for each point:
        //    points[j].X += Convert.ToInt32(PixelToShiftX);
        //    points[j].Y -= Convert.ToInt32(PixelToShiftY);        
        
        //calculate percentage of the positions respect to the range
        //note that max(rangeX, rangeY) will be "mapped" to the min(this.ClientSize.Width, this.ClientSize.Height)
        //so that we can handle different orientations 
        int i = 0;
        double percentX, percentY;

        //to make the calculations hemisphere-independent, calculate percentages with absolute values
        foreach (DataRow row in table.Rows)
        {
            percentX = Math.Abs(
                (Math.Abs(double.Parse(row[Column2].ToString(), frmMain.Instance.DataCultureInfo)) - Math.Abs(minX)) / 
                    Math.Max(rangeX, rangeY)
                );
            percentY = Math.Abs(
                (Math.Abs(double.Parse(row[Column1].ToString(), frmMain.Instance.DataCultureInfo)) - Math.Abs(minY)) / 
                    Math.Max(rangeX, rangeY)
                );

            //discard the point if NaN
            if (!(double.IsNaN(percentX) || double.IsNaN(percentY)))
            {
                //now fill the Point array: percentX and percentY are non-negative values
                //PixelToShiftX and PixelToShiftY allow to center the image
                points[i].X = Convert.ToInt32(ShortestSideOfTheDevice * percentX) + Convert.ToInt32(PixelToShiftX);
                points[i].Y = ShortestSideOfTheDevice - Convert.ToInt32(ShortestSideOfTheDevice * percentY) - Convert.ToInt32(PixelToShiftY);
                ++i;
            }
        }
    }
    #endregion

    #region ACTUAL DRAWING
    using (Graphics g = this.CreateGraphics())
    {
        using (SolidBrush b = new SolidBrush(Color.Indigo))
        {
            g.Clear(Color.White);
       
            //if showing speed, distance, altitude: draw a polygon
            if (!bPosition)
            {
                g.FillPolygon(b, points);
                using (Font f = new Font(FontFamily.GenericSansSerif, 10.0f, FontStyle.Bold))
                {
                    b.Color = Color.Black;
                    g.DrawString(string.Format("Max: {0} {1}", maxY.ToString("F3", frmMain.Instance.DataCultureInfo), unit), f, b, 0.0f, 0.0f);
                }
            }
            else //bPosition == true
            {
                //draw path area - this is a square independently on the device
                b.Color = Color.Honeydew;
                g.FillRectangle(b, 0, 0, ShortestSideOfTheDevice, ShortestSideOfTheDevice);
                
                //draw path grid, one line for each km (horizontal and vertical)
                double KmInRange = GpsLocation.CalculateLinearDistance(
                        new GpsLocation(0.0f, 0.0f, 0.0f, string.Empty, string.Empty),
                        new GpsLocation(0.0f, Math.Max(rangeX, rangeY), 0.0f, string.Empty, string.Empty));
        
                using (Pen p = new Pen(Color.PaleTurquoise, 0.2f))
                {
                    for (int i = 0; i < Math.Ceiling(KmInRange); i++)
                    {
                        g.DrawLine(p, 20 + i * Convert.ToInt32(ShortestSideOfTheDevice / KmInRange), 0, 20 + i * Convert.ToInt32(ShortestSideOfTheDevice / KmInRange), ShortestSideOfTheDevice);
                        g.DrawLine(p, 0, 20 + i * Convert.ToInt32(ShortestSideOfTheDevice / KmInRange), ShortestSideOfTheDevice, 20 + i * Convert.ToInt32(ShortestSideOfTheDevice / KmInRange));
                    }
                }
                
                //draw path
                using (Pen p = new Pen(Color.Blue, 1.0f))
                {
                    g.DrawLines(p, points);
                }

                //Start point
                b.Color = Color.Green;
                g.FillEllipse(b, points[0].X - 3, points[0].Y - 3, 6, 6); 
                
                //Stop point
                b.Color = Color.Red;
                g.FillEllipse(b, points[points.GetLength(0) - 1].X - 2, points[points.GetLength(0) - 1].Y - 2, 4, 4); 
            }
        }
    }
    #endregion

    Cursor.Current = Cursors.Default;
}

The results of that on different platforms are, for path and speed:

  • Smartphone, 320x240:

320x240_WM6Standard_path 320x240_WM6Standard_speed

  • Pocket PC, 240x240

240x240_WM6Pro_path  240x240_WM6Pro_speed

  • Pocket PC, 640x480 landscape

480x640_WM6Pro_path  480x640_WM6Pro_speed

NEXT STEP: now that I have played with representation of GPS data, I want to see how difficult it can be to show ON THE DEVICE a map by using Virtual Earth (through GPX file format)...

After that, now that I can really appreciate what the GPS Intermediate Driver can do for me (no need of Bluetooth and NMEA Intermediate layers!), I'll write a new version of the application: this time I'll also use SSCE 3.5 and probably the OrientationAware Application Block of the Mobile Client Software Factory... Smile ehy! I won't re-invent the wheel: check out this project on Codeplex!!

Cheers,

~raffaele

P.S. Check out what the MVP Maarten Struys has to say in this recent Level 300-Webcast!

Creating Location-Aware Applications for Windows Mobile Devices (Level 300)