Introduction
This code shows how to draw a compass and how to use it with a NMEA compatible GPS device.
Background
For me this was just a prelude for another project, testing out how to access GPS data from a NMEA device and it was fun to draw a Compass.
Using the code
You can easily modify the code to support your custom adruino electronic compass if you like. The compass class is independently from the data source reusable. Just call the static DrawCompass method to draw the compass.
pictureBox1.Image = Compass.DrawCompass(degree, pitch, 80, tilt, 80, pictureBox1.Size);
Points of Interest
I found quite annoying that NMEA devices seem not to send an identifier, so when opening Com ports to scan for one you really have to wait for the next data to arrive, parse it and if it is NMEA data accept it, and the close all other Com Ports, or you force the user configure the used Com Port to make it less userfriendly ;-). Important is to abort all open threads on Exiting the App, for else it won't exit due to the blocked threads. To have them Blocked at opening/closing all ports was necessary as it seems that the SerialPort Object seems not threadsafe in that it looses it's eventhandlers otherwise or the object disposes itself despite still having references, when opened from within another thread, a thread.Join seems to alleviate the issue but creating some few blocked threads for each closed port that still have to be aborted at the end of the application. I presume this approach could be used for other types of serial devices as well. Please send me a note if you find a better solution to this problem.
void DisconnectGPS()
{
if (serialPort1 != null)
{
try
{
if (serialPort1.IsOpen)
serialPort1.Close();
serialPort1.Dispose();
}
catch { }
} if (_Serial_Ports != null)
{
for (int i = _Serial_Ports.Length - 1; i >= 0; i--)
{
System.IO.Ports.SerialPort p = _Serial_Ports[i];
if (p != null)
{
try
{
if (p.IsOpen)
p.Close();
p.Dispose();
}
catch
{ }
}
}
}
foreach (Thread t in _Gps_Threads)
{
t.Abort();
}
}
void ConnectGPS()
{
String[] portnames = System.IO.Ports.SerialPort.GetPortNames();
_Serial_Ports = new System.IO.Ports.SerialPort[portnames.Length];
_Gps_Threads = new Thread[portnames.Length];
for (int i = 0; i < portnames.Length; i++)
{
System.IO.Ports.SerialPort ssp = new System.IO.Ports.SerialPort(portnames[i]);
try
{
object data0 = (object)new object[] { ssp, i };
System.Threading.Thread t1 = new Thread(delegate(object data)
{
System.IO.Ports.SerialPort sspt1 = (System.IO.Ports.SerialPort)((object[])data)[0];
int it1 = (int)((object[])data)[1];
_Serial_Ports[it1] = sspt1;
try
{
sspt1.DataReceived += serialPort1_DataReceived;
sspt1.Open();
}
catch
{ }
System.Threading.Thread.Sleep(3000);
try
{
foreach (System.IO.Ports.SerialPort sspt2 in _Serial_Ports.Where(r => !r.PortName.Equals(serialPort1.PortName)))
{
if (sspt2.IsOpen)
sspt2.Close();
sspt2.Dispose();
}
}
catch
{ }
System.Threading.Thread.CurrentThread.Join();
});
_Gps_Threads[i] = t1;
t1.Start(data0);
}
catch { }
}
}
To identify a NMEA compatible device you just read out what's coming from the port. Does it match your expected NMEA sentences, then you got it and set your serialPort1=p. The below code works for Magellan Explorist devices. Not sure if GPRMC sentence is provided with all NMEA device. You can easily modify it to match it your device. NMEA Sentence Specs that worked for me, I found here: http://aprs.gids.nl/nmea/
It's pretty easy to work out I'd say. Consider that the NMEA device may loose the satelite connection and you may get blanks from the device in those lines.
void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
try
{
System.IO.Ports.SerialPort p = ((System.IO.Ports.SerialPort)sender);
string data = p.ReadExisting();
string[] strArr = data.Split('$');
for (int i = 0; i < strArr.Length; i++)
{
string strTemp = strArr[i];
string[] nmea = strTemp.Split(',');
if (nmea[0] == "GPRMC")
{
serialPort1 = p;
if (!String.IsNullOrEmpty(nmea[8]) )
{
degree = Convert.ToDouble(nmea[8]);
pitch = 0;
tilt = 0;
pictureBox1.Image = Compass.DrawCompass(degree, pitch, 80, tilt, 80, pictureBox1.Size);
}
}
}
}
catch
{}
}
To draw the Compass itself is pretty straight forward. Just using a little bit of geometry and DrawLine, DrawElipse, DrawString... see below what you can do with these simple commands. Most amazing about it is that it runs in only a few milliseconds (on 2 year old my notebook its only 5ms per DrawCompass).
To draw the compass onto an existing Bitmap just modify the method to not use "result" but to use the passed on Bitmap and use the size of the existing bitmap or whichever size you want he compass to be and to modify it's location on the bitmap is easy enough by modifying the xcenterpoint and ycenterpoint.
Pitch and Tilt is not used when with a GPS device, as you get your heading from the device's location and movement, but if you use a magnetic compass (i.e. adruino device) you may want to show the pitch or tilt when calibrating, so I added the pitch and tilt so you can roll it, just like on your smart phone and see how it deviates from a 0 degree pitch / tilt roll.
public class Compass
{
public static Bitmap DrawCompass(double degree, double pitch, double maxpitch, double tilt, double maxtilt, Size s)
{
double maxRadius = s.Width > s.Height ? s.Height / 2 : s.Width / 2;
double sizeMultiplier = maxRadius / 200;
double relativepitch = pitch / maxpitch;
double relativetilt = tilt / maxtilt;
Bitmap result=null;
SolidBrush drawBrushWhite = new SolidBrush(Color.FromArgb(255, 244, 255));
SolidBrush drawBrushRed = new SolidBrush(Color.FromArgb(240, 255, 0, 0));
SolidBrush drawBrushOrange = new SolidBrush(Color.FromArgb(240, 255, 150, 0));
SolidBrush drawBrushBlue = new SolidBrush(Color.FromArgb(100, 0, 250, 255));
SolidBrush drawBrushWhiteGrey = new SolidBrush(Color.FromArgb(20, 255, 255, 255));
double outerradius = (((maxRadius - sizeMultiplier * 60) / maxRadius) * maxRadius);
double innerradius = (((maxRadius - sizeMultiplier * 90) / maxRadius) * maxRadius);
double degreeRadius = outerradius + 37 * sizeMultiplier;
double dirRadius = innerradius - 30 * sizeMultiplier;
double TriRadius = outerradius + 20 * sizeMultiplier;
double PitchTiltRadius = innerradius * 0.55;
if (s.Width * s.Height > 0)
{
result=new Bitmap(s.Width, s.Height);
using (Font font2 = new Font("Arial", (float)(16 * sizeMultiplier)))
{
using (Font font1 = new Font("Arial", (float)(14 * sizeMultiplier)))
{
using (Pen penblue = new Pen(Color.FromArgb(100, 0, 250, 255), ((int)(sizeMultiplier) < 4 ? 4 : (int)(sizeMultiplier))))
{
using (Pen penorange = new Pen(Color.FromArgb(255, 150, 0), ((int)(sizeMultiplier) < 1 ? 1 : (int)(sizeMultiplier))))
{
using (Pen penred = new Pen(Color.FromArgb(255, 0, 0), ((int)(sizeMultiplier) < 1 ? 1 : (int)(sizeMultiplier))))
{
using (Pen pen1 = new Pen(Color.FromArgb(255, 255, 255), (int)(sizeMultiplier * 4)))
{
using (Pen pen2 = new Pen(Color.FromArgb(255, 255, 255), ((int)(sizeMultiplier) < 1 ? 1 : (int)(sizeMultiplier))))
{
using (Pen pen3 = new Pen(Color.FromArgb(0, 255, 255, 255), ((int)(sizeMultiplier) < 1 ? 1 : (int)(sizeMultiplier))))
{
using (Graphics g = Graphics.FromImage(result))
{
double sourcewidth = s.Width;
double sourceheight = s.Height;
int xcenterpoint = (int)(s.Width / 2);
int ycenterpoint = (int)((s.Height / 2));
Point pA1 = new Point(xcenterpoint, ycenterpoint - (int)(sizeMultiplier * 45));
Point pB1 = new Point(xcenterpoint - (int)(sizeMultiplier * 7), ycenterpoint - (int)(sizeMultiplier * 45));
Point pC1 = new Point(xcenterpoint, ycenterpoint - (int)(sizeMultiplier * 90));
Point pB2 = new Point(xcenterpoint + (int)(sizeMultiplier * 7), ycenterpoint - (int)(sizeMultiplier * 45));
Point[] a2 = new Point[] { pA1, pB1, pC1 };
Point[] a3 = new Point[] { pA1, pB2, pC1 };
g.DrawPolygon(penred, a2);
g.FillPolygon(drawBrushRed, a2);
g.DrawPolygon(penred, a3);
g.FillPolygon(drawBrushWhite, a3);
double[] Cos = new double[360];
double[] Sin = new double[360];
g.DrawLine(pen2, new Point(((int)(xcenterpoint - (PitchTiltRadius - sizeMultiplier * 50))), ycenterpoint), new Point(((int)(xcenterpoint + (PitchTiltRadius - sizeMultiplier * 50))), ycenterpoint));
g.DrawLine(pen2, new Point(xcenterpoint, (int)(ycenterpoint - (PitchTiltRadius - sizeMultiplier * 50))), new Point(xcenterpoint, ((int)(ycenterpoint + (PitchTiltRadius - sizeMultiplier * 50)))));
Point PitchTiltCenter = new Point((int)(xcenterpoint + PitchTiltRadius * relativetilt), (int)(ycenterpoint - PitchTiltRadius * relativepitch));
int rad = (int)(sizeMultiplier * 8);
int rad2 = (int)(sizeMultiplier * 25);
Rectangle r = new Rectangle((int)(PitchTiltCenter.X - rad2), (int)(PitchTiltCenter.Y - rad2), (int)(rad2 * 2), (int)(rad2 * 2));
g.DrawEllipse(pen3, r);
g.FillEllipse(drawBrushWhiteGrey, r);
g.DrawLine(penorange, PitchTiltCenter.X - rad, PitchTiltCenter.Y, PitchTiltCenter.X + rad, PitchTiltCenter.Y);
g.DrawLine(penorange, PitchTiltCenter.X, PitchTiltCenter.Y - rad, PitchTiltCenter.X, PitchTiltCenter.Y + rad);
for (int d = 0; d < 360; d++)
{
double angleInRadians = ((((double)d) + 270d) - degree) / 180F * Math.PI;
Cos[d] = Math.Cos(angleInRadians);
Sin[d] = Math.Sin(angleInRadians);
}
for (int d = 0; d < 360; d++)
{
Point p1 = new Point((int)(outerradius * Cos[d]) + xcenterpoint, (int)(outerradius * Sin[d]) + ycenterpoint);
Point p2 = new Point((int)(innerradius * Cos[d]) + xcenterpoint, (int)(innerradius * Sin[d]) + ycenterpoint);
if (d % 30 == 0)
{
g.DrawLine(penblue, p1, p2);
Point p3 = new Point((int)(degreeRadius * Cos[d]) + xcenterpoint, (int)(degreeRadius * Sin[d]) + ycenterpoint);
SizeF s1 = g.MeasureString(d.ToString(), font1);
p3.X = p3.X - (int)(s1.Width / 2);
p3.Y = p3.Y - (int)(s1.Height / 2);
g.DrawString(d.ToString(), font1, drawBrushWhite, p3);
Point pA = new Point((int)(TriRadius * Cos[d]) + xcenterpoint, (int)(TriRadius * Sin[d]) + ycenterpoint);
int width = (int)(sizeMultiplier * 3);
int dp = d + width > 359 ? d + width - 360 : d + width;
int dm = d - width < 0 ? d - width + 360 : d - width;
Point pB = new Point((int)((TriRadius - (15 * sizeMultiplier)) * Cos[dm]) + xcenterpoint, (int)((TriRadius - (15 * sizeMultiplier)) * Sin[dm]) + ycenterpoint);
Point pC = new Point((int)((TriRadius - (15 * sizeMultiplier)) * Cos[dp]) + xcenterpoint, (int)((TriRadius - (15 * sizeMultiplier)) * Sin[dp]) + ycenterpoint);
Pen p = penblue;
Brush b = drawBrushBlue;
if (d == 0)
{
p = penred;
b = drawBrushRed;
}
Point[] a = new Point[] { pA, pB, pC };
g.DrawPolygon(p, a);
g.FillPolygon(b, a);
}
else if (d % 2 == 0)
g.DrawLine(pen2, p1, p2);
if (d % 90 == 0)
{
string dir = (d == 0 ? "N" : (d == 90 ? "E" : (d == 180 ? "S" : "W")));
Point p4 = new Point((int)(dirRadius * Cos[d]) + xcenterpoint, (int)(dirRadius * Sin[d]) + ycenterpoint);
SizeF s2 = g.MeasureString(dir, font1);
p4.X = p4.X - (int)(s2.Width / 2);
p4.Y = p4.Y - (int)(s2.Height / 2);
g.DrawString(dir, font1, d == 0 ? drawBrushRed : drawBrushBlue, p4);
}
}
String deg = Math.Round(degree, 2).ToString("0.00") + "°";
SizeF s3 = g.MeasureString(deg, font1);
g.DrawString(deg, font2, drawBrushOrange, new Point(xcenterpoint - (int)(s3.Width / 2), ycenterpoint - (int)(sizeMultiplier * 40)));
}
}
}
}
}
}
}
}
}
}
return result;
}
}
The Truth about North
I intentionally called it a "simple Compass" not to open the can of worms regarding all the other topics of navigation. But now that the can has been opened we may as well mention and point out is that GPS devices determine one's heading usually by calculating the vector between the GPS Fix points. Correct me if I am wrong, that meaning that a GPS compass will work only as long as you are moving, unless it has a secondary magnetic sensor, or other means of determining the heading (i.e. cell tower triangulation, wireless network information, etc.) built in too. Magnetic north however would need to be corrected to calculate True North for navigational purposes considering magnetic declination and magnetic deviation. (see http://en.wikipedia.org/wiki/Magnetic_deviation)
Electronic compasses furthermore require recalibration to compensate for the environmental deviations.
GPS compass Pros: True north
GPS compass Cons: No heading when stationary, Accuracy depending satellite data receptions and its interpretation
Magnetic Compass Pros: Headings when stationary, based on Earth's magnetic fields
Magnetic Compass Cons: Inaccuracies deviating from True North due to magnetic declination and deviation
i.e. my analogue magnetic scuba diving compasses deviate towards my torch (it having magnetic switches), and my European Scuba diving compass is off by roughly 5 degrees here in Australia.
Clearly I am not an expert at this matter, but it's mentioned now. The Compass.DrawCompass feature will work accurately and correclty in either way. Whether your data input is accurate for your purposes remains within your control choosing your most suitable Compass - data source and adjusting the data against your individually applicable declination/deviation before drawing the Compass.
History
2014-05-26 Added a point of interest as suggested by SteveHolle regarding the "truth" about north and device specific caveats on that.