地图控件MapControl的底层实现

手写地理信息组件系列 第11篇
MapControl的设计&优化
难度指数:★★★☆☆

感谢读者一直以来的陪伴,欢迎继续回到我们的GIS底层话题。

MapControl是什么

在GIS组件中,距离用户最近的图形控件,可以说非MapControl莫属了。

MapControl就是地图控件,是界面中地图数据的显示容器,提供对地图数据的各种可视操作。缩放、漫游、图形编辑等等都由MapControl作为入口执行。

GDI+ (Graphics Device Interface plus 图形设备接口),是一种处理Windows程序的图形输出技术。简单来说就是提供了一组绘图接口,图形绘制的实现交由系统及具体设备绘制输出。

GDI+ 在C#中体现为一组绘图函数。而地图的绘制也就基于这些基本的绘图函数,具体的方式,我们下边会有展开。

地图数据如何绘制

地图数据最初从shapefile中读取,主要是经过以下过程最终呈现出来的:

  1. 读取shp坐标点,获取地理坐标(经纬度)。

  2. 将地理坐标转换为投影坐标。也就是从球面坐标转换为平面坐标,这样才能在一个平面展示球面上的图形。

  3. 根据当前地图窗口的尺寸和地理范围,对投影坐标点做比例尺计算,形成窗口内可绘制的屏幕坐标。

  4. 将屏幕坐标传入控件自身提供的绘图函数,绘制出图。

过程总结起就是:

*地理坐标->投影坐标->屏幕坐标->绘图

这里的地理坐标不是必须的,有的图形存储投影坐标,所以无需转换。

地图绘制的概念调用链

软件的复杂性要求我们要对概念有明确的把握,以方便清晰的对功能进行分层。一个概念做一个概念的事,职责明确,依赖最小。

在图层的本质中,通过对Geometry、Feature的梳理,引出了独立的图层概念。

在之前的设计中,绘图函数被安排在各类型图形对象自身中,需要对绘图函数传入Graphics和Map对象。

对功能来说,绘制出图没有问题。问题在于:绘图函数的出现,污染了Geometry的纯粹性。导致任何需要Geometry的地方,都必须引入Graphics和Map这两个无关依赖,麻烦的很。

相对MapControl绘图来说,Geometry的层次属于的是最底层。底层不能依赖于高层,这是模块化设计的准则

Geometry、Feature、Layer、Map、MapControl

那么,到底绘图这一应用应该出现在哪个概念中?

在这一概念链条中,毫无疑问哪个环节都能实现绘图,因为他们都能持有数据。问题是放哪里最合理?这一度让人陷入摇摆。

放在Geometry里,可以更容易的对每个图形的绘制风格做更细粒度的控制,代价是会对其概念造成一定侵入。

放在MapControl里,可以更方便对全局的图形绘制做统一管理。但是,对更多类型图形绘制的管理也会更复杂,每增加一种图层的类型,都需要对MapControl做出修改。依然得不偿失。

通过参阅各个商业软件的API和开源软件源码,发现在绘图这一问题上,都把图层(Layer)视为地图绘图的基本单元

因为图层在意义上与一个shp文件相对应,管理着一组图形。图层可以直接的对这一组图形进行显示。典型的的例子如:对点图层进行符号化。根据城市点图形的人口属性,以不同的大小来绘制这组点图形,获得直观显示效果。
在这里插入图片描述

所以,绘图函数最合适的位置,应该是出现在Layer层面。下不入侵基本的图形和要素概念;同时,若有新增的图形或影像数据类型,就创建新的图层类型,并结合上层对象给予的"画板",构造自己的绘图逻辑,绘制到画板上就行了。

这样,就是做到对上层的影响最小,也是结构设计的水平可扩展。

概念实现

以下是Layer类在持有Feature集合的基础上,增加的绘图函数。目前只有矢量数据,所以只有点线面的绘图函数。若有影像、DEM或更多类型数据,就需要扩展Layer的类型,例如ImageLayer,TileLayer等等,只要新设计一个ILayer让他们继承就好。

public void Draw(Graphics g, Map map)
{
    if (g == null || map == null)
       return;

    switch (shapeType)
    {
      case ShapeType.NullShape:
        break;
      case ShapeType.Point:
      DrawPoint(g, map);

        break;
      case ShapeType.Line:
      DrawLine(g, map);

        break;
      case ShapeType.Polygon:
      DrawPolygon(g, map);

        break;
      default:
        break;
     }
}

//点绘图
public void DrawPoint(Graphics g, Map map)
{
   foreach (var f in features)
   {
     //map对象通知停止渲染,即引发渲染中止异常
     if (!map.IsRenderingEnabled)
       throw new RenderingAbortedException("Point Drawing Aborted");

     Vertex p = f.GetGeometry().Centroid;
     System.Drawing.Point point = map.ToScreenPoint(p);

     g.FillEllipse(new SolidBrush(Color.Orange),
       new Rectangle(point.X, point.Y, 4, 4));
    }
}

//线绘图
public void DrawLine(Graphics g, Map map)
{
   foreach (var f in features)
   {
     if (!map.IsRenderingEnabled)
      throw new RenderingAbortedException("Line Drawing Aborted");

     Line line = f.GetGeometry() as Line;
     System.Drawing.Point[] points =
       new System.Drawing.Point[line.Vertexes.Count];

     for (int i = 0; i < points.Length; i++)
     {
       //地理-屏幕坐标转换
       points[i] = map.ToScreenPoint(line.Vertexes[i]);
     }
     g.DrawLines(new Pen(Color.WhiteSmoke, 2), points);
    }
}

//面绘图
public void DrawPolygon(Graphics g, Map map)
{
   foreach (var f in features)
   {
     if (!map.IsRenderingEnabled)
       throw new RenderingAbortedException("Polygon Drawing Aborted");

     Polygon polygon = f.GetGeometry() as Polygon;

     System.Drawing.Point[] points =
             new System.Drawing.Point[polygon.Vertexes.Count];

     for (int i = 0; i < points.Length; i++)
     {
       points[i] = map.ToScreenPoint(polygon.Vertexes[i]);
     }

     g.FillPolygon(
       new SolidBrush(Color.LightSeaGreen), points);

     g.DrawLines(new Pen(Color.Brown, 1.5f), points);
   }
}

地图类Map,除拥有对地图坐标和屏幕坐标转换的函数外,要负责对图层集合的绘图函数做调用

方法是遍历类内部持有的Layer集合,执行其Draw函数。并且其内部持有了两个变量isRenderingisRenderingEnabled

第一变量指示当前绘图任务的执行状态;第二变量用于控制绘图是否允许进行,这里主要用来对绘图任务进行中止,在不需要的时候,及时退出逐层绘图这一相对耗时的过程。

// 正在渲染的标志变量
volatile bool isRendering = false;

// 允许渲染的标志变量
volatile bool isRenderingEnabled = true;

public bool IsRendering
{
  get { return isRendering; }
}

public bool IsRenderingEnabled
{
  get { return isRenderingEnabled; }
}

// 终止渲染
public void AbortRendering()
{
  isRenderingEnabled = false;
}

/// <summary>
/// 地图渲染
/// </summary>
/// <param name="g">一个"画板"</param>
public void Render(Graphics g)
{
 try
 {
   foreach (Layer lyr in layers)
   {
     isRendering = true;
     lyr.Draw(g, this);
   }
 }
 finally
 {
   isRenderingEnabled = true;
   isRendering = false;
 }
}

/// <summary>
/// 终止渲染引发的异常
/// </summary>
public class RenderingAbortedException : Exception
{
   public RenderingAbortedException(String message)
   : base(message)
   {
   }
}

MapControl的构造

MapControl首先是一个Control,在C#中通过继承UserControl类就能获得一个基本的可视控件。

布局很简单,控件本身+底部停靠的状态栏,状态栏左下角,被设计用来实时显示鼠标地理位置坐标。

谈到实时位置坐标显示,就必然离不开事件,继承UserControl的最大好处就是可以复用函数,只要在父类的鼠标移动处理函数之后,加入我们自己的坐标转换逻辑就能做到。下面来看:

//定义鼠标在地图上移动的事件
 public event MouseMapMoveEventHandle MouseMapMove;
 
 public delegate void MouseMapMoveEventHandle
   (Object sender, MouseMapMoveEventArgs e);
 
 //复写UserControl的OnMouseMove
 protected override void OnMouseMove(MouseEventArgs e)
 {
    base.OnMouseMove(e);
    this.OnMouseMapMove(e);
 }

protected virtual void OnMouseMapMove(MouseEventArgs e)
{
  if (_map != null && _map.Layers.Count > 0)
  {
    Vertex p = _map.ToMapVertex(
      new System.Drawing.Point(e.X, e.Y));

    this.locationLabel.Text =
      String.Format("X:{0} Y:{1}",
      Math.Round(p.x, 3), Math.Round(p.y, 3));

    // 如果外部订阅了鼠标地理位置改变事件
    // 每动一次鼠标就产生一个此事件
    if (MouseMapMove != null)
      MouseMapMove(this, 
           new MouseMapMoveEventArgs(p.x, p.y));
  }
}

定义鼠标在地图控件移动时的事件参数:

public class MouseMapMoveEventArgs : EventArgs
{
 double x;
 double y;

 public MouseMapMoveEventArgs(double x, double y)
 {
   this.x = x;
   this.y = y;
}

public double X
{
  get { return x; }
}

public double Y
{
  get { return y; }
}
}

类似的,地图控件还应该有一个地图图形被选中的事件,典型的使用情况是点击图形显示属性,这一事件也是MapControl逃不掉的职责,这就来定义它:

 //图形选中事件
 public event GeometrySelectedEventHandle GeometrySelected;
 
 public delegate void GeometrySelectedEventHandle
   (Object sender, GeometrySelectedEventArgs e);
 
 //选中事件肯定对应UserControl的点击事件
 //复写之
 protected override void OnMouseClick(MouseEventArgs e)
{
  base.OnMouseClick(e);

  if (GeometrySelected != null)
    OnGeometrySelected(e);
}

//模拟图形对象选中事件
protected virtual void OnGeometrySelected(MouseEventArgs e)
{
  if (_map != null && _map.Layers.Count > 0)
  {
    if (GeometrySelected != null)
    {
      Vertex p = _map.ToMapVertex(
        new System.Drawing.Point(e.X, e.Y));

      GeometrySelected(this,
        new GeometrySelectedEventArgs((int)p.x));
    }
  }
}

图形对象选中事件参数,返回的是图形id:

public class GeometrySelectedEventArgs
 {
   int geoID;
 
   public GeometrySelectedEventArgs(int geoID)
   {
     this.geoID = geoID;
   }
   public int GeoID
   {
    get { return this.geoID; }
   }
}

图形对象选中事件是做的模拟处理,图形选中是一个相对复杂的过程,不是这篇的讨论范畴,我们就先按下不表。。

MapControl的核心逻辑

明人不说废话,MapControl核心逻辑就是绘图。

实现绘图的条件是MapControl持有一个Map对象,Map对象继续持有下层对象,逐层读取实现绘制。

流程是明确的,一个调用一个。但效率是一个很大问题。每缩放一次或者Resize,界面都要等到每个图层绘制完成才能响应,这是不能忍受的。
这里的解决方法是多线程延迟执行+旧线程中断。具体来看下边的解决方式。

public MapControl()
 {
   InitializeComponent();
   this.DoubleBuffered = true;
   this.BackgroundImageLayout = ImageLayout.None;
   _map = new Map();
 }
 
 private Map _map;

public int MapWidth
{
  get { return this.Width; }
}

public int MapHeight
{
  get
  {
    return this.Height
      - this.statusStrip.Height;
  }
}

public Map Map
{
  get { return _map; }
  set { _map = value; }
}

volatile int reqTime = -1;

public void RenderMap(Extent extent)
{
  reqTime = Environment.TickCount;

  if (extent == null)
  {
    if (_map.Layers.Count < 1)
      return;

    _map.UpdateExtent(_map.Layers[0].Extent,
      new Rectangle(0, 0, MapWidth, MapHeight));
    extent = _map.Extent;
  }

  Thread t = new Thread(
    new ParameterizedThreadStart(RenderMapThread));

  //创建一个以当前时间命名的绘图线程
  t.Priority = ThreadPriority.Highest;
  t.Name = reqTime.ToString();
  t.Start(extent);

}

public void RenderMap()
{
  RenderMap(_map.Extent);
}

//延迟处理绘图请求
//如果短时间内有多次绘图请求
//只执行最新的一次请求
protected void RenderMapThread(Object extent)
{
  Thread.Sleep(200);

  if (int.Parse(Thread.CurrentThread.Name) != reqTime)
    return;

  RenderMapInternal((Extent)extent);
}

Object lockObj = new object();

private void RenderMapInternal(Extent extent)
{
  try
  {
    //中止正在绘图的线程
    if (_map.IsRendering)
      _map.AbortRendering();

    //当前线程独占式启动绘图 
    lock (lockObj)
    {
      //声明一个空白图片对象
      Bitmap image = new Bitmap(MapWidth, MapHeight);
      Graphics g = Graphics.FromImage(image);

      g.SmoothingMode = SmoothingMode.HighQuality;
      _map.UpdateExtent(extent,
        new Rectangle(0, 0, MapWidth, MapHeight));

      //将地图绘制到空白图片上
      _map.Render(g);

      g.Dispose();

     //图片贴回控件
     this.BackgroundImage = image;

    // 如果绘图完成之后,发现窗口尺寸已经变化,
     // 则按新尺寸再画一次
     if (image.Width != MapWidth
       || image.Height != MapHeight)
     {
       RenderMapInternal(extent);
     }
   }
 }
 catch ()
 {
 }
}

调用层面测试

初步封装的MapControl现在可以使用了,它会出现在VS的工具箱中,可以拖拽到自己的界面上。

现在来看一下,要实现一次地图绘制,需要在客户调用层面做什么事情。

 private void btn_OpenShp_Click(object sender, EventArgs e)
 {
   OpenFileDialog ofd = new OpenFileDialog();
   ofd.Filter = "(*.shp)|*.shp";
   if (ofd.ShowDialog() == DialogResult.OK)
   {
     ShapeFile shp = new ShapeFile();
 
     Layer layer = shp.ReadShapeFile(ofd.FileName);

    //MapControl加入图层
    mapControl1.Map.Layers.Add(layer);

    //渲染图层
    this.mapControl1.RenderMap();
  }
}

调用还是相对简单的。读取图层,加入图层,显示图层三步走。

这里是以上逻辑的显示效果:

在Form中订阅对象选定事件:

this.mapControl1.GeometrySelected += mapControl1_GeometrySelected;

void mapControl1_GeometrySelected(object sender, 
     GeometrySelectedEventArgs args)
{
   MessageBox.Show(args.GeoID.ToString());
}

可以实现上文模拟的图形选择:

弹出数值是鼠标位置的地图x坐标,拿到了鼠标地图坐标,对象选择就不远了。

本篇完。

上一篇:shp文件的属性就是个Excel ??



  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值