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... [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.
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... 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:
- "Adapt Your App" main page
- the Webcast Series presented by Jim Wilson
- and the OrientationAware Application Block of the Microsoft Mobile Client Software Factory
- ScreenLib
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:
- Pocket PC, 240x240
- Pocket PC, 640x480 landscape
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... 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)
Comments
- Anonymous
May 31, 2009
PingBack from http://outdoorceilingfansite.info/story.php?id=649