一、 地图是怎么做出来的
首先说一下地图是怎么出来的,可能你感觉是废话,但实际上很多人并不知道如何下手。我觉得这里需要先给你个思路准备:地图就是使用绘图语句画出来的!
从底层绘制地图,能使用的就是绘图函数,在.NET里,就是用Graphics类的方法,在窗口中绘制点、线、面、标准、栅格等,组合起来,就是一张地图(瓦片图方式除外)。
关于.NET的绘图,本文不进行讲解,如果你还还不熟悉,建议你先看看这方面资料。
二、 坐标转换-地图绘制的关键
.NET提供了大量绘图方法,基本上都是以Graphics类的函数形式提供,包括各类几何形状、图像、文字的绘制,灵活运用这些方法,就可以画出精美的图出来。假设你已熟悉.NET的绘图,这样就只有有一个问题要解决:图我会画了,但拿到地图元素一般为地理坐标(经纬度),应该画在地图上什么位置?这就需要涉及到坐标转换问题。
先不考虑怎么实现,首先需要这么一个函数:
/// <summary>
/// 经纬度转换为屏幕坐标
/// </summary>
/// <param name="xy">经纬度</param>
/// <returns>屏幕坐标</returns>
public Point WorldToScreen(PointF xy)
再一个,有时,还需要根据屏幕上点位置反算出它的经纬度,如在需要显示鼠标指针处的经纬度,所以还需要这么一个函数:
/// <summary>
/// 屏幕坐标转换为经纬度
/// </summary>
/// <param name="xy">屏幕坐标</param>
/// <returns>经纬度</returns>
public PointF ScreenToWorld(Point xy)
有了这两个函数,就可以将以经纬度表示的地理坐标转换为屏幕坐标,然后再屏幕绘图了。
为了完成坐标转换,需要使用几个地图参数的变量:地图缩放倍数、地图中心点经纬度、地图大小,关于地图参数,可参考这篇文章:
http://hi.baidu.com/geochenyj/blog/item/6b5c5c1294057557f819b835.html
另外,还需要对地图进行缩放、平移,这些操作实质上也是对地图参数的操作,如放大就是对地图缩放倍数操作,平移就是对地图中心点进行操作,我们将这些操作也写Coordinator类的方法。投影变换也作为坐标转换的一部分,Coordinator类还增加了投影方面方法,这个后面再讲。
将上面两个坐标转换函数和三个地图参数封装为一个类Coordinator。,的类如下所示:
三、 绘图
有了坐标转换类Coordinator,就可以用经纬度数据来绘图了,如拿到某省的行政边界经纬度坐标数据,就可以将经纬度数据转换为屏幕坐标,然后用Graphics的方法来画出来了,Graphics对象又从哪里来呢?可以从一个Image对象创建,也可以从一个控件的Paint事件中取得,总之,有了坐标,发挥你的想象力,自己画吧。
在气象数据分析中,除了要绘制点、线、面、文字、栅格外,还需要绘制一些特殊符号,如风、天气现象、云等。这些符号,可以用图片、天气字库、符号库来实现,图片方式实现简单,色彩丰富,但缩放效果不好;字库方式,需要安装字库,程序部署比较麻烦;符号库方式代码编写较麻烦。FreeMicaps的天气现象符号采用符号库方式,祥见:
风符号和云量符号采用计算坐标绘制方式。
为了使用方便,FreeMicaps把符号绘制功能封装到三个符号类中,以静态方法提供。
.NET的绘图是对GDI+的封装,包括了对点、线、面等各种图形元素的封装,图形图像的绘制、坐标旋转,各种反走样和平滑等功能,功能十分强大(当然,效率不太高),利用它可以绘出漂亮的图形。
根据OGC标准,GIS系统首先需要对地图元素进行抽象和封装,但FreeMicaps中,经再三考虑,放弃了这种方式,一个是因为工作量比较大,另一个是因为我不敢保证能很好地进行封装,可能给插件开发带来麻烦,不如把绘图权完全交给图层,大家自由发挥。
四、 图层
为了使绘图过程便于管理,可将绘图过程分为组,如可以将一张地图的绘制分为:绘制世界地图、绘制中国地图、绘制河流、填地名几个过程,每次绘图好像就是在一张玻璃上绘制,叠加起来就形成了一张地图图,这里把每次绘图过程形象地称为一个图层。地图分层后,图层可以增删,每个图层可以单独进行隐藏、设置属性等,更重要的是可以将利用面向对象技术把每个图层当做一个对象进行管理。详细介绍见:
对图层进行抽象,它应该有一个图层绘制方法(Render),一个图层标题(LayerName),一个用于表示数据源的字符串(DataSource),一个用于表示绘图样式的设置的LayerStyle,加上一些辅助方法属性,最终形成如下抽象图层类(CustomLayer),各种图层均从它继承:
FreeMicaps中,每种数据对应一种图层类,为了使图层类编写方便,使用了设计模式中的模板方法,定义绘制流程,主程序在调用图层的Render()方法时,会自动判断是否已经读入数据,根据需要读数据绘图。
对于一种类型数据,需要从CustomLayer继承新建一个图层类。各种类型数据图层的工作方式完全一样,仅在数据读取和绘制方面不同,所以,写新图层类时,仅需实现DoLoad()和DoRender()两个抽象方法,完成读取数据和绘制图层代码即可。FreeMicaps里使用了字符串作为数据源标识,通用GIS系统对数据源进行了抽象,我也尝试这么做,但代码过于复杂,增加图层开发难度,最终增大插件开发难度,所以放弃了。
前面说了,一张地图有多个图层,所以还需要将图层放入一个列表,绘制地图时遍历图层,调用每个图层的Render()方法,画出一张完整的地图。对于图层列表,大家马上会想到使用List类,但图层绘制是需要有顺序的,如在卫星云图上面叠加地名,需要先画卫星云图,再填地名,否则云图会把地名盖住,所以在图层的样式(LayerStyle)中放了一个ZOrder属性,通过它来控制图层顺序。但由于List本身的排序方法是一种“非稳固排序”,也就是说当两个图层的ZOrder相等时,它们的顺序是不确定的,为了避免这个问题,FreeMicaps从CollectionBase继承了一个类LayerList,实现对图层的管理,并实现了IXmlSerializable接口,完成图层序列化功能。另外,还增加了添加图层、删除图层事件。LayerList类如下:
FreeMicaps中,每种数据对应一种图层类,为了使图层类编写方便,使用了设计模式中的模板方法,定义绘制流程,主程序在调用图层的Render()方法时,会自动判断是否已经读入数据,根据需要读数据绘图。
对于一种类型数据,需要从CustomLayer继承新建一个图层类。各种类型数据图层的工作方式完全一样,仅在数据读取和绘制方面不同,所以,写新图层类时,仅需实现DoLoad()和DoRender()两个抽象方法,完成读取数据和绘制图层代码即可。FreeMicaps里使用了字符串作为数据源标识,通用GIS系统对数据源进行了抽象,我也尝试这么做,但代码过于复杂,增加图层开发难度,最终增大插件开发难度,所以放弃了。
前面说了,一张地图有多个图层,所以还需要将图层放入一个列表,绘制地图时遍历图层,调用每个图层的Render()方法,画出一张完整的地图。对于图层列表,大家马上会想到使用List类,但图层绘制是需要有顺序的,如在卫星云图上面叠加地名,需要先画卫星云图,再填地名,否则云图会把地名盖住,所以在图层的样式(LayerStyle)中放了一个ZOrder属性,通过它来控制图层顺序。但由于List本身的排序方法是一种“非稳固排序”,也就是说当两个图层的ZOrder相等时,它们的顺序是不确定的,为了避免这个问题,FreeMicaps从CollectionBase继承了一个类LayerList,实现对图层的管理,并实现了IXmlSerializable接口,完成图层序列化功能。另外,还增加了添加图层、删除图层事件。LayerList类如下:
五、 封装地图
有了坐标转换类、图层类、图层列表类,就可以利用它们做出一个具有缩放平移、图层管理等功能的地图了,但为了更方便地对地图进行操作,还需要对这些类进行组合封装。新建一个类WeatherMap,添加Coordinator和LayerList类的实例作为它的属性,为了更符合大家操作习惯,将Coordinator类的实例作为私有成员,将地图坐标转换等方法加入WeatherMap类,也就是说地图坐标转换中,不访问Coordinator,而要调用WeatherMap类的方法。类图如下:
再回到抽象图层类CustomLayer,它有一个成员Map,即为WeatherMap对象,在将图层加入图层列表时会自动赋值。在编写CustomLayer的子类时,可调用它来进行坐标转换和地图操作。
为了使地图在绘制复杂图形过程中不至于假死,并在绘图过程中能随时中断绘图,如快速缩放平移地图中可终止前次绘图过程直接绘制最后一次,地图绘制使用了多线程,但多线程增加了代码编写难度,特别是多线程操作UI,对程序流程造成了一定混乱,程序结构受到影响,所幸并不会对图层代码造成困难。
六、 再次封装-增加UI
上面已完成了地图绘制的核心代码,为了使代码编写更加容易,需要对WeatherMap类再次进行封装(MapView类),加入UI部分,即给地图加一个具有界面的壳,并在上面实现地图的操作如缩放、拖动功能。
MapView从PictureBox类继承,内建了WeatherMap类的实例,在MapView的Refresh()方法中调用WeatherMap.Render()对地图进行绘制。
为了完成对地图的操作,FreeMicaps定义一个IMapTool接口,包含了鼠标和键盘操作方法,MapView类内建一个IMapTool接口成员,MapView的鼠标和键盘操作,将被IMapTool接口的实例接管,在实现IMapTool接口的类中,可对地图做各种操作,如平移、缩放等操作,这个对象可随时替换以实现不同方式的地图操作。在FreeMicaps中,已完成一个实现IMapTool接口的类ZoomTool,此类为默认的地图缩放和平移工具。IMapTool接口类图如下:
另外,在MapView中,还引入了一个当前图层的概念CurrentLayer,用它来表示当前操作的图层,后面用它来实现图层元素拾取、图层工具条等功能。
MapView类图如下:
七、 总览
地图部分类关系图如下:
地图绘制部分活动图如下:
以上已经介绍完FreeMicaps地图部分设计框架,相信大家的已对设计思路已有一定了解,此框架不仅适用于天气图分析软件,也适用于一般的GIS系统。本文仅对FreeMicaps的地图部分框架进行了介绍,未涉及到具体的地图数据读取及绘制,这些将在下一篇文章中介绍。