2022年5月15日 星期日

[Window Form] GMap的客製marker

  
    internal class CustomMarker : GMapMarker
    {
        //GMapMarker沒有Font屬性,所以自己新增
        private Font Font { get; set; } = new Font(FontFamily.GenericSansSerif, 8f, FontStyle.Bold);

        public CustomMarker(PointLatLng pos) : base(pos)
        {
            Size = new Size(20, 20);
            //Marker置中,點擊位置為中心
            Offset = new Point(-Size.Width / 2, -Size.Height / 2);
            Tag = "9";
        }

        public override void OnRender(Graphics g)
        {
            base.OnRender(g);
            var x = LocalArea.X;
            var y = LocalArea.Y;
            var w = LocalArea.Width;
            var h = LocalArea.Height;
            g.FillEllipse(Brushes.White, LocalArea);
            g.DrawEllipse(Pens.Black, LocalArea);
            //這使用Tag屬性記錄文字內容,也可使用其他屬性
            var tag = Tag?.ToString();
            //計笡文字大小
            var textSize = TextRenderer.MeasureText(tag, Font);
            g.DrawString(tag, Font, Brushes.Black, new PointF(x + (w - textSize.Width) / 2f + 1.8f, y + (h - textSize.Height) / 2f));
        }
    }

使用方法:

    Form1.cs
    //設定一組經緯座標,也可是滑鼠點擊的位置轉換而來(FromLocalToLatLng)
    //var position =  gMapControl1.FromLocalToLatLng(int x, int y);
    var position = new PointLatLng(24.064, 120.704);
    var marker = new CustomMarker(position);
    var overlay1 = new GMapOverlay("mark");
    overlay1.Markers.Add(marker);
    gMapControl1.Overlays.Add(overlay1);

2022年5月5日 星期四

[Window Form] GMap常用指令

初始化常用參數:

    //設定地圖提供者
    gMapControl1.MapProvider = GMap.NET.MapProviders.GMapProviders.OpenStreetMap;//較多可選擇
    gMapControl1.MapProvider = GMap.NET.MapProviders.GoogleChinaHybridMapProvider.Instance;
    //是否使用緩存
    GMaps.Instance.Mode = AccessMode.ServerAndCache;
    //設定缓存位置
    gMapControl1.CacheLocation = @"C:\GMapCache";
    //地圖縮放最小比例
    gMapControl1.MinZoom = 1;
    //地圖縮放最大比例
    gMapControl1.MaxZoom = 22;
    //地圖縮放當前比例
    gMapControl1.Zoom = 7;
    //是否顯示中心十字
    gMapControl1.ShowCenter = true;
    //設定地圖拖拽按鍵
    gMapControl1.DragButton = MouseButtons.Right;
    //設定地圖中心經緯度
    gMapControl1.Position = new PointLatLng(24.064, 120.704);

GMap可以任意增加圖層(Overlay),圖層上可增加任意標示(Marker)。

    //增加Marker,有內建的Marker,給予經緯度座標。
    var marker = new GMarkerGoogle(new PointLatLng(24.064, 120.704), GMarkerGoogleType.yellow_small);
    //建立裝載Marker用的Overlay,如果要區分不同Marker,可以建立不同Overlay
    var overlay = new GMapOverlay("marker");
    overlay.Markers.Add(marker);
    gMapControl1.Overlays.Add(overlay);

圖層可增加

  • GMapPolygon
  • GMapRoute
  • GMarkerGoogle

GMapRoute:

    //設定目標點位
    List<PointLatLng> points= new List<PointLatLng>
    {
        new PointLatLng(10,20),
        new PointLatLng(30,40)
    };
    //設定筆刷大小及樣式(預設是實線)
    var pen = new Pen(Color.Red, 2)
    {
        DashStyle = System.Drawing.Drawing2D.DashStyle.Dash//虛線
    };
    //建立Route
    var route = new GMapRoute(points, "route")
    {         Stroke = pen,     };     //建立Overlay     var overlay = new GMapOverlay("routes");     overlay.Routes.Add(route);     gMapControl1.Overlays.Add(overlay);

GMapPolygon:

    與GMapRoute相似,只是不會規劃路線,以及多了封閉選項。
    封閉選項 => 是否要讓最後的點位會連接到第一個點位。

座標轉換:

    //螢幕座標轉經緯度
    var latLng = gMapControl1.FromLocalToLatLng(int x, int y);
    //經緯度轉螢幕座標
    var pos = gMapControl1.FromLatLngToLocal(new PointLatLng(24.064, 120.704));

2022年5月3日 星期二

[Window Form] 載入GMap

使用NuGet下載GMap必需的相關檔案。
    工具 => NuGet套件管理員 => 管理方案的NuGet套件


在瀏覽中搜尋GMap,安裝其中的GMap.NET.Windows,它會自動去下載相依的檔案,
如:GMap.NET.Core、GMap.NET.WinForms、GMap.NET.Presentation...等。


下載完後就可以在設計模式(Design)下的工具箱中找到GMapControl。

拉出GMapControl後還需要設定一些參數才能正確顯示地圖。
還沒設定前的GMapControl:

設定前的參數:

設定 this.gMapControl1.MaxZoom = 22;
         this.gMapControl1.MinZoom = 2;
         this.gMapControl1.Zoom = 5D;
         //使用預設的大小Size(150, 150),可能小於Tile的大小,顯示會不完全
         this.gMapControl1.Size = new System.Drawing.Size(300, 300);
設定後的參數:



在建構式中設定:
    //使用何種地圖
    gMapControl1.MapProvider = GMap.NET.MapProviders.GoogleChinaHybridMapProvider.Instance;
    //是否使用緩存
    GMaps.Instance.Mode = AccessMode.ServerAndCache;
    //設定最初顯示的座標
    gMapControl1.Position = new PointLatLng(24.064, 120.704);
設定前的GMapControl:


2022年4月28日 星期四

[Window Form] 繪製地面站姿態儀(九) 使用SerialPort連接飛控

 我們使用Mission Planner提供的Example來進行Serial Port連線,

並取得飛控中的姿態資訊後與我們之前完成的UI結合在一起。


首先先下載Mission Planner的Source Code。

打開Mission Planner的資料夾中的SimpleExample。

路徑 => MissionPlanner-master\ExtLibs\SimpleExample

在我們的UI中增加兩個ComboBox,一個選擇Serial Port,一個選擇BaudRate,

最後再加一個Button1當作Connect。

設定兩個ComboBox,
ComboBox1取得系統上的Serial Port

    private void comboBox1_MouseClick(object sender, MouseEventArgs e)
    {
        //取得系統上的Comport
        comboBox1.DataSource = SerialPort.GetPortNames();
    }

在建構式中設定ComboBox2,手動增加BaudRate

    var list = new List <int> { 19200,115200 };
    comboBox2.DataSource = list;

BaudRate可以參考Mission Planner的常用BaudRate,

但實際值還是要看飛控上的設定為主。


載入MAVLink:
    在SimpleExample執行後會產生一個exe檔與相關的dll,
    我們只需要將生成的MAVLink.dll引入到專案中就可以使用MAVLink了。
    參考 => (右鍵)加入參考 => 瀏覽 =>
    (下載的SourceCode => MissionPlanner-master\ExtLibs\SimpleExample\bin\Debug)
    選取 MAVLink.dll => 確定




設定Button

    private void Button1_Click(object sender, EventArgs e)
    {
        if (serialPort1.IsOpen)
            serialPort1.Close();
        serialPort1.PortName = comboBox1.Text;
        serialPort1.BaudRate = int.Parse(comboBox2.Text);
        serialPort1.Open();
        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += Worker_DoWork;
        worker.RunWorkerAsync();
    }

    private MAVLink.MavlinkParse mavlink = new MAVLink.MavlinkParse();
    private object readLock = new object();
    private byte sysid;
    private byte compid;

    private void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        while (serialPort1.IsOpen)
        {
            try
            {
                MAVLink.MAVLinkMessage packet;
                lock (readLock)
                {
                    packet = mavlink.ReadPacket(serialPort1.BaseStream);
                    if (packet == null || packet.data == null) continue;
                }

                if (packet.data.GetType() == typeof(MAVLink.mavlink_heartbeat_t))
                {
                    var hb = (MAVLink.mavlink_heartbeat_t)packet.data;

                    // save the sysid and compid of the seen MAV
                    sysid = packet.sysid;
                    compid = packet.compid;

                    // request streams at 2 hz
                    var buffer = mavlink.GenerateMAVLinkPacket10(MAVLink.MAVLINK_MSG_ID.REQUEST_DATA_STREAM,
                        new MAVLink.mavlink_request_data_stream_t()
                    {
                        req_message_rate = 2,
                        req_stream_id = (byte)MAVLink.MAV_DATA_STREAM.ALL,
                        start_stop = 1,
                        target_component = compid,
                        target_system = sysid
                    });

                    serialPort1.Write(buffer, 0, buffer.Length);

                    buffer = mavlink.GenerateMAVLinkPacket10(MAVLink.MAVLINK_MSG_ID.HEARTBEAT, hb);

                    serialPort1.Write(buffer, 0, buffer.Length);
                }

                if (sysid != packet.sysid || compid != packet.compid)
                    continue;

                //Console.WriteLine(packet.msgtypename);

                if (packet.msgid == (byte)MAVLink.MAVLINK_MSG_ID.ATTITUDE)
                //or
                //if (packet.data.GetType() == typeof(MAVLink.mavlink_attitude_t))
                {
                    var att = (MAVLink.mavlink_attitude_t)packet.data;
                    var roll = att.roll * 57.2958;
                    var yaw = att.yaw * 57.2958;
                    var pitch = att.pitch * 57.2958;
                    userControl1.Roll = (float)roll;
                    userControl1.Yaw = (float)yaw;
                    userControl1.Pitch = (float)pitch;
                    //Console.WriteLine($@"roll => {roll}  yaw => {yaw}  pitch => {pitch}");
                }
            }
            catch (Exception)
            {
                throw;
            }
            Thread.Sleep(1);
        }
    }

2022年4月17日 星期日

[Window Form] 繪製地面站姿態儀(八) Yaw刻度加入文字

在Yaw刻度上增加文字,

每45度代表不同方位,

飛控回傳的值有可能會超過+-360度,

所以必須做出相對應的處理。

    #region Yaw
    graphics.ResetTransform();
    //增加Yaw背景
    var yawBg = new RectangleF(0, 0, Width, Height / 2f / 7.5f);
    using (var brush = new SolidBrush(Color.FromArgb(128, 255, 255, 255)))
    {
        graphics.FillRectangle(brush, yawBg);
    }
    //只顯示+-60度
    var start = Yaw - 60f;
    var end = Yaw + 60f;
    var boundaryWidth = Width / 15f;//邊界
    var space = (Width - boundaryWidth) / 120f;//間隔
    var lineHeight = yawBg.Height * 2 / 5f;//刻度
    //畫刻度(start要轉型成int,不然如果是double或float會畫不出來)
    for (var a = (int)start; a <= end; a++)
    {
        if (a % 5 == 0)
        {
            //每5度為一刻度
            var posX = yawBg.Left + boundaryWidth / 2f + (a - start) * space;
            var posY = yawBg.Bottom - lineHeight / 2f;
            using (var pen = new Pen(Color.White, 2f))
            {
                graphics.DrawLine(pen, posX, posY, posX, posY - lineHeight);
            }
            //刻度加上文字,每45度為一方位角
            if (a % 15 == 0)
            {
                var disp = a;
                while (disp < 0) disp += 360;//將角度轉換為正向角
                disp %= 360;
                string directionString;
                switch (disp)
                {
                    case 0:
                        directionString = "N";
                        break;
                    case 45:
                        directionString = "NE";
                        break;
                    case 90:
                        directionString = "E";
                        break;
                    case 135:
                        directionString = "SE";
                        break;
                    case 180:
                        directionString = "S";
                        break;
                    case 225:
                        directionString = "SW";
                        break;
                    case 270:
                        directionString = "W";
                        break;
                    case 315:
                        directionString = "NW";
                        break;
                    default:
                        directionString = disp.ToString();
                        break;
                }
                posY -= Height/2f/15f;//10
                DrawString(directionString.PadLeft(5), new Font(Font.FontFamily, fontSize), new SolidBrush(Color.White), posX, posY, graphics, StringAlignment.Center);
            }
        }
    }
    //刻度底線
    using (var pen = new Pen(Color.White, 2f))
    {
        graphics.DrawLine(pen,
            yawBg.Left + boundaryWidth / 2f,
            yawBg.Bottom - lineHeight / 2f,
            yawBg.Right - boundaryWidth / 2f,
            yawBg.Bottom - lineHeight / 2f);
    }
    #endregion

2022年4月15日 星期五

[Window Form] 繪製地面站姿態儀(七) Roll刻度加入文字

在Roll的刻度上方加入文字,

並且讓一些數字與長寬連動,之後變更長寬時仍會維持原本的比例。

    #region Roll
    int[] angles = { -60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60 };
    //要顯示的角度陣列
    var angleHeight = 25f * offset;
    //畫角度的啟始高度,最高的Pitch刻度再高一些(在這是+20度外加位移5,所以取25)
    var angleLine = offset;//4.5
    //線條的長度
    foreach (var angle in angles)
    {
        graphics.ResetTransform();
        graphics.TranslateTransform(Width / 2f, Height / 2f);
        graphics.RotateTransform(angle + Roll);
        using (var pen = new Pen(Color.White, 2f))
        {
            graphics.DrawLine(pen, 0, -angleHeight, 0, -angleHeight - angleLine);
            //旋轉畫面後再畫刻度,線條往上畫,所以減線條長度值
            DrawString(angle.ToString(),
                new Font(Font.FontFamily, fontSize),
                new SolidBrush(Color.White),
                offset / 5 * 3.75f, -angleHeight - angleLine * 2,
                graphics, StringAlignment.Center);
        }
    }
    graphics.ResetTransform();
    graphics.TranslateTransform(Width / 2f, Height / 2f);
    var arcRect = new RectangleF(-angleHeight, -angleHeight, angleHeight * 2, angleHeight * 2);
    //因為刻度都是以angleHeight為半徑,
    //所以Rectangle的XY以(angleHeight,angleHeight)為啟始點
    //長寬為angleHeight的兩倍,正方矩形。
    using (var pen = new Pen(Color.White, 2f))
    {
        graphics.DrawArc(pen, arcRect, -150f + Roll, 120f);
        //顯示角度為+-60,所以全長為120度,然後向左旋轉150度
    }

    graphics.ResetTransform();
    graphics.TranslateTransform(Width / 2f, Height / 2f);
    using (var pen = new Pen(Color.Red, 2f))
    {
        //畫三角指標
        var arrowWidth = 8f;
        var points = new PointF[3];
        points[0] = new PointF(0, -angleHeight);
        points[1] = new PointF(arrowWidth, -angleHeight + arrowWidth);
        points[2] = new PointF(-arrowWidth, -angleHeight + arrowWidth);
        graphics.DrawPolygon(pen, points);
    }
    #endregion

[Window Form] 繪製地面站姿態儀(六) Pitch刻度加入文字

上篇增加了文字的繪製,

現在要將文字新增到Pitch刻度的旁邊。

    #region Pitch
    graphics.TranslateTransform(Width / 2f, Height / 2f);
    graphics.RotateTransform(Roll);
    var longEdge = Width / 2f / 5f;//30
    var shortEdge = longEdge - 10;
    var offset = Height / 2 / 2f / 15 / 5f * 4.5f;//4.5
    var fontSize = Width / 2f / 10f / 5f * 4;//12
    for (var a = -90; a <= 90; a += 5)
    {
        //每5度為一刻度,以Pitch為中心只顯示+-20度
        if (a <= Pitch + 20 && a >= Pitch - 20)
        {
            using (var pen = new Pen(Color.White, 3f))
            {
                //每10度加強顯示
                var edge = a % 10 == 0 ? longEdge : shortEdge;
                graphics.DrawLine(pen, 
                    new PointF(-edge + 0, 0 + (Pitch - a) * offset), 
                    new PointF(edge + 0, 0 + (Pitch - a) * offset));
                DrawString(a.ToString(),
                    new Font(Font.FontFamily, fontSize),
                    new SolidBrush(Color.White),
                    -longEdge, (Pitch - a) * offset,
                    graphics, StringAlignment.Right);
            }
        }
    }

    graphics.ResetTransform();
    graphics.TranslateTransform(Width / 2f, Height / 2f);
    graphics.RotateTransform(Roll);//指標跟著Roll旋轉
    var centerRec = new RectangleF(-Width / 4f, -Height / 4f, Width / 2f, Height / 2f);
    using (var pen = new Pen(Color.Red, 4f))
    {
        //刻度指標
        graphics.DrawLine(pen, new PointF(0, 0), new PointF(-longEdge, longEdge / 2f));
        graphics.DrawLine(pen, new PointF(0, 0), new PointF(longEdge, longEdge / 2f));
        //水平指標
        graphics.DrawLine(pen, new PointF(centerRec.Left - longEdge, 0), new PointF(centerRec.Left, 0));
        graphics.DrawLine(pen, new PointF(centerRec.Right + longEdge, 0), new PointF(centerRec.Right, 0));
    }
    #endregion