前言
GMap.NET https://greatmaps.codeplex.com/ 是一个强大, 免费, 跨平台, 开源的. NET 控件. 分为 WPF 和 winform 版. GMap.NET 的基本知识不做过多介绍, 本文主要介绍如何使用该控件实现电子围栏功能.
电子围栏主要有两个功能模块: 界面展示围栏区域, 判断人员出入围栏的逻辑. GMap.NET 的 WPF 版本功能并不强大, 实现一些复杂的功能就只能发掘 WPF 的潜力了. GMap.NET 给我们提供了一个基本的平台, 必须熟练掌握 WPF 才能开发出复杂 gis 产品.
围栏区域界面显示
1 认识 GMapMarker
GMapControl 是地图的主容器; 地图就是多个图片拼接而来, 这个图片组成 GMapControl 的底图. 底图之上点缀用户自定义的控件. 用户自定义控件必须通过 GMapMarker 间接添加进来, 看下面代码:
- GMapMarker maker = new GMapMarker(ptLatLng);
- //UserControlFence 用户自定控件
- _ctrlCurrentFence = new UserControlFence() { Marker = maker, MapCtrl = MainMap };
- _ctrlCurrentFence.FenceInfo = CreateFenceInfoModel();
- maker.Shape = _ctrlCurrentFence;
- this.MainMap.Markers.Add(maker);
GMapMarker 的定义也不复杂:
- public class GMapMarker : INotifyPropertyChanged
- {
- public object Tag;
- public GMapMarker(PointLatLng pos);
- public UIElement Shape { get; set; }
- public PointLatLng Position { get; set; }
- public GMapControl Map { get; }
- public Point Offset { get; set; }
- public int LocalPositionX { get; }
- public int LocalPositionY { get; }
- public int ZIndex { get; set; }
- public event PropertyChangedEventHandler PropertyChanged;
- public virtual void Clear();
- protected void OnPropertyChanged(string name);
- protected void OnPropertyChanged(PropertyChangedEventArgs name);
- }
一个 GMapMarker 关联一个 gps 坐标, 同时可以显示一个控件 (Shape ); 为什么在 Shape 外面包含一个 marker?maker 主要功能就是将控件钉到 GMapControl 的一个点. 当地图移动时, maker 会做相应的移动, maker 移动会带动 shape 移动. 所以, 我们只管把 shape 内部处理好就行了, 不用管地图移动. maker 的作用不大, 并不能帮我们实现复杂的功能; Shape 才是我们施展拳脚的地方.
2 用户控件实现画图
在控件中 UserControlFence 实现电子围栏的绘制, 该控件会关联到 maker 的 shape.UserControlFence 控件以 Grid(name 为 gridRoot) 布局; WPF 的 Path 可以实现任意图像的绘画, 首先要将 Path 加入到 Grid. 我们的输入是多个 gps 点坐标, 怎么能转换成 Path 上各个点坐标? 这需要经过多次转换;
- Point ToCtrlPoint(PointLatLng gpsPoint)
- {
- // 转换成 GMap.NET 控件坐标
- GPoint ptOfMapCtrl = MapCtrl.FromLatLngToLocal(gpsPoint);
- //GMap.NET 控件坐标要转换成 控件相对于直接父面板的坐标
- Point ptToMapCtrl2 = new Point(ptOfMapCtrl.X, ptOfMapCtrl.Y);
- // 转成屏幕坐标
- Point ptOfScreen = MapCtrl.PointToScreen(ptToMapCtrl2);
- // 转换成相对于 gridRoot 的坐标
- Point ptOfParentPanel = gridRoot.PointFromScreen(ptOfScreen);
- return ptOfParentPanel;
- }
转换过程就是: 相对于 Map 控件坐标 --> 屏幕坐标 --> 相对于 Grid 的坐标. 因为 Path 是 Grid 的 Child, 最后的坐标也是相对于 Grid 的坐标. 用该坐标绘制 Path, 就是电子围栏的区域;
Path 的 Data 是 Geometry, 生成 Geometry 函数如下:
- private PathGeometry CreatPath()
- {
- if (_listPoints.Count <= 1)
- {
- PathRouteLine.Data = null;
- return null;
- }
- List<Point> listPt = ListWndPoint;
- PathFigure pathFigure = new PathFigure();
- pathFigure.StartPoint = listPt[0]; // 起始点
- pathFigure.IsClosed = true;
- for (int i = 1; i <listPt.Count; i++)
- {
- // 加入线段
- LineSegment line = new LineSegment() { Point = listPt[i] };
- pathFigure.Segments.Add(line);
- }
- PathGeometry geometry = new PathGeometry();
- geometry.Figures.Add(pathFigure);
- return geometry;
- }
这样就完成电子围栏的区域绘制. 还有一点要注意: 当地图缩放时, 必须重新绘制. 地图缩放比例不同, 绘制区域大小也会改变 (形状不会变). 只需要监视地图控件的事件 public event MapZoomChanged OnMapZoomChanged; 就行.
出入电子围栏区域判断
该判断逻辑有多种实现方法, 下面逐一介绍;
1 利用 WPF 的辅助函数 VisualTreeHelper.HitTest
通过判断 gps 点坐标是否在控件内来判断. gps 坐标先要转成控件点坐标 (转换函数见前文). 函数实现比较简单;
- private bool IsInFence(PointLatLng gpsPoint)
- {
- if (_listPoints.Count <= 2)
- return false;
- Point ptWnd = ToCtrlPoint(gpsPoint);
- HitTestResult result = VisualTreeHelper.HitTest(gridRoot, ptWnd);
- if (result == null || result.VisualHit==null)
- return false;
- bool hit = result.VisualHit == PathRouteLineInner;
- return hit;
- }
2 通过 GraphicsPath,Region 实现
这是 System.Drawing 下的一组类, 属于微软早期的类库; 该类的点坐标还是 float 型, 精度不高. 对于 gps 坐标我先做了放大处理, 如果不做处理误差会很大.
- private bool IsInFence2(PointLatLng gpsPoint)
- {
- double rate = 100000; // 由于 float 精度问题. 对坐标放大处理, 否则误差会很大.
- System.Drawing.Drawing2D.GraphicsPath pointPath = new System.Drawing.Drawing2D.GraphicsPath();
- System.Drawing.PointF[] points = _listPoints.Select(o => new System.Drawing.PointF((float)(o.Lng * rate), (float)(o.Lat * rate))).ToArray();
- pointPath.AddLines(points);
- pointPath.CloseFigure();
- System.Drawing.Region region = new System.Drawing.Region(pointPath);
- System.Drawing.PointF ptHit = new System.Drawing.PointF((float)(gpsPoint.Lng * rate), (float)(gpsPoint.Lat * rate));
- bool visible = region.IsVisible(ptHit);
- return visible;
- }
3 直接根据点坐标计算
理论上这种方式效率是最高的, 并且不依赖界面控件. 但是这种方法不是微软提供的, 准确性还需要验证. 下面的函数是从网上找的, 我对此计算结果做了验证, 与前两种计算方法的结果一致的.
- private bool IsInFence3(PointLatLng gpsPoint)
- {
- int count = _listPoints.Count;
- if (count <3)
- {
- return false;
- }
- bool result = false;
- for (int i = 0, j = count-1; i < count; i++)
- {
- var p1 = _listPoints[i];
- var p2 = _listPoints[j];
- if (p1.Lat < gpsPoint.Lat && p2.Lat>= gpsPoint.Lat || p2.Lat <gpsPoint.Lat && p1.Lat>= gpsPoint.Lat)
- {
- if (p1.Lng + (gpsPoint.Lat - p1.Lat) / (p2.Lat - p1.Lat) * (p2.Lng - p1.Lng) < gpsPoint.Lng)
- {
- result = !result;
- }
- }
- j = i;
- }
- return result;
- }
后记电子围栏区域绘制方法与轨迹回放, 测距等处理有类似之处; GMap.NET 为我们做的工作并不多, 关键是要掌握处理这一类问题的精髓, 做到举一反三, 许多问题就会迎刃而解.
来源: https://www.cnblogs.com/yuanchenhui/p/gis_fence.html