简单的指南针






4.98/5 (17投票s)
如何绘制一个指南针并为其提供 NMEA 设备数据(即 GPS 指南针)
引言
此代码展示了如何绘制指南针以及如何将其与兼容 NMEA 的 GPS 设备一起使用。
背景
对我来说,这只是另一个项目的序曲,用于测试如何从 NMEA 设备访问 GPS 数据,并且绘制指南针很有趣。
使用代码
您可以轻松修改代码以支持您自定义的 adruino 电子指南针。指南针类可以独立于数据源重用。只需调用静态 DrawCompass 方法即可绘制指南针。
pictureBox1.Image = Compass.DrawCompass(degree, pitch, 80, tilt, 80, pictureBox1.Size);
关注点
我发现 NMEA 设备似乎不发送标识符,这很令人恼火,所以在打开 Com 端口扫描时,您确实必须等到下一个数据到来,解析它,如果它是 NMEA 数据则接受它,然后关闭所有其他 Com 端口,或者强制用户配置使用的 Com 端口,使其不那么用户友好;-)。 重要的是在退出应用程序时中止所有打开的线程,否则由于线程被阻塞而无法退出。在打开/关闭所有端口时让它们被阻塞是必要的,因为似乎 SerialPort 对象在其他情况下会丢失其事件处理程序,或者尽管仍有引用但对象会自行销毁,并且从另一个线程打开时,thread.Join 似乎可以缓解这个问题,但创建一些用于每个关闭的端口的被阻塞线程,这些线程仍然必须在应用程序结束时中止。我假设这种方法也可能用于其他类型的串行设备。如果您发现此问题的更好解决方案,请给我留言。
void DisconnectGPS()
{
if (serialPort1 != null)
{
try
{
if (serialPort1.IsOpen)
serialPort1.Close();
serialPort1.Dispose();
}
catch { }
// serialPort1 = null;
} 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
{ }
// p = null;
}
}
}
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);
// t1.Join();
}
catch { }
}
}
要识别兼容 NMEA 的设备,您只需读出端口传输的数据。它是否与您预期的 NMEA 语句匹配,那么您就找到了它,并设置您的 serialPort1=p。下面的代码适用于 Magellan Explorist 设备。不确定 GPRMC 语句是否随所有 NMEA 设备提供。您可以轻松地对其进行修改以匹配您的设备。NMEA 语句规范,对我来说有效,我在以下位置找到:http://aprs.gids.nl/nmea/
我认为这很容易解决。考虑到 NMEA 设备可能会丢失卫星连接,您可能会从设备中的这些行中获得空白。
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") //Modify to your needs This works with Magellan Explorist GC series
{
serialPort1 = p;
if (!String.IsNullOrEmpty(nmea[8]) )
{
degree = Convert.ToDouble(nmea[8]);// -(lines[11] == "E" ? 1 : -1) * (String.IsNullOrEmpty(lines[10]) ? 0 : Convert.ToDouble(lines[10]));//if the device shows true north, else use the deviation to compensate
pitch = 0;
tilt = 0;
pictureBox1.Image = Compass.DrawCompass(degree, pitch, 80, tilt, 80, pictureBox1.Size);
}
}
}
}
catch
{}
}
绘制指南针本身非常简单。只需使用一点几何学和 DrawLine、DrawElipse、DrawString...看看使用这些简单的命令您可以做什么。最令人惊奇的是它只运行几毫秒(在我 2 年前的笔记本上,每次 DrawCompass 只有 5 毫秒)。
要将指南针绘制到现有位图上,只需修改该方法,使其不使用“result”而是使用传入的位图,并使用现有位图的大小或您希望指南针的大小,并通过修改 xcenterpoint 和 ycenterpoint 来修改其在位图上的位置就足够了。
使用 GPS 设备时,不使用俯仰和倾斜,因为您从设备的当前位置和移动中获取航向,但如果您使用磁罗盘(例如 adruino 设备),您可能希望在校准时显示俯仰或倾斜,因此我添加了俯仰和倾斜,这样您就可以滚动它,就像在智能手机上一样,并查看它与 0 度俯仰/倾斜滚动的偏差。
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))
{
// Calculate some image information.
double sourcewidth = s.Width;
double sourceheight = s.Height;
int xcenterpoint = (int)(s.Width / 2);
int ycenterpoint = (int)((s.Height / 2));// maxRadius;
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];
//draw centercross
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)))));
//draw pitchtiltcross
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);
//prep here because need before and after for red triangle.
for (int d = 0; d < 360; d++)
{
// map[y] = new long[src.Width];
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);
//Draw Degree labels
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);
//draw N,E,S,W
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);
//}
////Draw red triangle at 0 degrees
//if (d == 0)
//{
}
}
//draw course
//g.DrawLine(pen1, new Point(xcenterpoint, ycenterpoint - (int)innerradius), new Point(xcenterpoint, ycenterpoint - ((int)outerradius + (int)(sizeMultiplier * 50))));
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;
}
}
关于北方的真相
我故意称之为“简单的指南针”,以免打开导航所有其他主题的“潘多拉魔盒”。但既然已经打开了这个盒子,我们不妨提及并指出,GPS 设备通常通过计算 GPS 定位点之间的向量来确定一个人的航向。如果我错了,请纠正我,这意味着 GPS 指南针只能在您移动时工作,除非它有辅助磁传感器,或其他确定航向的方法(例如,蜂窝塔三角测量、无线网络信息等)内置。然而,出于导航目的,为了计算真北,磁北需要进行修正,考虑到磁偏角和磁偏航。(见http://en.wikipedia.org/wiki/Magnetic_deviation)
此外,电子罗盘需要重新校准以补偿环境偏差。
GPS 指南针优点:真北
GPS 指南针缺点:静止时无航向,精度取决于卫星数据接收及其解释
磁罗盘优点:静止时的航向,基于地球磁场
磁罗盘缺点:由于磁偏角和磁偏航,与真北的偏差不准确
例如,我的模拟磁性水肺潜水罗盘会偏向我的手电筒(它有磁性开关),而且我的欧洲水肺潜水罗盘在澳大利亚这里大约偏离 5 度。
显然,我不是这方面的专家,但现在已经提到了。Compass.DrawCompass 功能无论哪种方式都能准确无误地工作。您选择最适合您的指南针 - 数据源并根据您个人适用的偏角/偏航调整数据,然后再绘制指南针,其输入数据的准确性将取决于您。
历史
2014-05-26 根据 SteveHolle 的建议,添加了关于北方的“真相”以及设备特定注意事项的兴趣点。