FreeMicaps开发讲解二: 图层
上一讲对FreeMicaps的地图框架进行了介绍,未涉及到具体数据和绘制,这一讲将对数据读取、地图渲染做讲解。
FreeMicaps为插件式架构,用户可按它的插件接口编写代码对所支持的数据类型进行扩展。数据类型扩展代码最主要的就是是针对新增数据编写相应的图层类,所以本节偏重于图层类的具体实现,为插件开发做准备。
再次对地图框架进行总结,N个地图元素->图层,N个图层->天气图,天气图+UI->地图视图。结构图概括如下:
MapView是从PaintBox继承下来的一个类,利用IMapTool处理其鼠标和键盘操作;当它进行绘制时,调用WeatherMap的Render()方法,WeatherMap的Render遍历图层调调用图层的Render方法,各图层又遍历它们的地图元素调用地图元素的Render方法,最终组合形成成一张地图。WeatherMap的Render()方法如下:
/// <summary>
/// 绘制所有图层
/// </summary>
/// <param name="graphics"></param>
public void Render(Graphics graphics)
{
for (int i = 0; i < Layers.Count; i++)
{
CustomLayer layer = Layers[i];
if (_StopRending)
{
break;
}
layer.Render(graphics);
}
}
可以看出,地图绘制,就演变为图层的绘制,图层的绘制,又归结到了地图元素的绘制。对于本系统所需的天气图绘制来说,地图元素不光是地图的点、线、面、标注、栅格,还包括各类形式各异的符号、图形等,根据不同数据类型有不同的表现。所以地图的绘制就从图层讲起。
一、图层的基类-CustomLayer
各种图层虽然形式多样,但其核心只是数据加载和数据渲染过程不同,而数据加载还可以归为数据渲染里,数据可以在数据渲染过程中根据需要加载。所以,图层抽象类的核心方法只有一个,即Render(Graphics graphics)方法,在WeatherMap的地图渲染中,也只是调用了图层的Render()方法。
虽说核心如此,但图层的抽象类不能这么简单,还是需要增加一些属性,如数据源字符串、是否可视、图层名称等,另外,还增加了一个LayerStyle的属性,此属性里包含了图层的绘制颜色、字体等参数,便于从外部控制图层的渲染参数。最终形成如下抽象类CustomLayer:
CustomLayer的Render()方法为虚方法,可以在子类中进行重写以实现具体图层的具体绘制过程。你可能还会注意到,还有两个Protected的抽象方法DoLoad()和DoRender(),这两又是干什么的呢?刚才我们说了,数据可在图层渲染过程按需进行加载,在CustomLayer的Render方法里,首先判断了数据是否数据是否已加载,如果没加载则调用抽象方法DoLoad(),这个过程可让数据只在第一次渲染时加载,子类不用管何时该加载,只需分别实现数据加载方法DoLoad()和数据渲染方法DoRender()就行了。使用这种方法的好处是对于小型地图可以加快地图渲染速度,但对大型地图,如一张地图几十上百兆就不适合了,解决办法是自己实现Render()方法控制数据加载。
CustomLayer的Render方法如下:
/// <summary>
/// 渲染图层
/// </summary>
/// <param name="graphics"></param>
public virtual void Render(Graphics graphics)
{
//读入数据
if (!IsLoaded)
{
_Loaded = true;
DoLoad();
}
//绘图
DoRender(graphics);
}
二、图层子类简单实现
第一节说过,对每一种数据类型,都对应一个CustomLayer的子类,所以要实现某种图层,可以从CustomLayer(或CustomLayer的资料)继承一个一个类,CustomLayer已为图层的数据加载和渲染搭好框架,在子类中仅根据需要实现DoLoad()和DoRender()方法即可。
要说完全所有图层都直接从CustomLayer继承,显然是不合适的,如Shp格式地图、Micaps格式地图、Bln格式地图,它们的渲染过程是一样的,只是读数据过程不同,所以FreeMicaps内核还提供了MapLayer(矢量地图图层基类)、ImageLayer(栅格地图基类)、HighLayer(高空填图基类)、SurfaceLayer(地面填图基类)、GridLayer(格点数据基类)等,这些基类继承CustomLayer,已实现DoRender()方法,开发新图层只需根据图层种类继承上面的图层实现DoLoad()方法即可。
举例来说,MapLayer类如下所示:
/// <summary>
/// 地图图层抽象基类(矢量图层,点线面)
/// </summary>
public abstract class MapLayer : CustomLayer
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="source">数据源字符串</param>
public MapLayer(string source)
: base(source)
{
LayerStyle = new MapStyle();
}
/// <summary>
/// 地图元素
/// </summary>
protected IList<MapModel> MapObjects = new List<MapModel>();
/// <summary>
/// 载入数据抽象方法(被DoLoad方法调用)
/// </summary>
protected abstract void LoadData();
/// <summary>
/// 载入数据
/// </summary>
protected override void DoLoad()
{
MapObjects.Clear();
LoadData();
Process();
}
private void Process()
{
//投影变换
foreach (MapModel mo in MapObjects)
{
for (int i = 0; i < mo.Points.Length; i++)
{
mo.Points[i] = Map.WorldToProj(mo.Points[i]);
}
}
}
/// <summary>
/// 渲染图层
/// </summary>
/// <param name="graphics"></param>
protected override void DoRender(Graphics graphics)
{
//MapStyle style = LayerStyle as MapStyle;
MapRender itemRender = new MapRender(this, LayerStyle);
MapStyle ms = LayerStyle as MapStyle;
if (ms != null) itemRender.IsMask = ms.IsMask;
foreach (MapModel mo in MapObjects)
{
itemRender.DrawObj = mo;
itemRender.Render(graphics);
}
}
}
在MapLayer图层类中,为了再次加快图层渲染速度,在读取数据后即进行地图元素坐标投影转换,避免在每次地图渲染中都进行计算。为了使流程更清楚,又增加了抽象方法LoadData()方法,继承MapLayer的图层实现此方法来读取数据。
为了说明如何增加一种新地图格式支持,我花半小时写了一个BLN格式(surfer格式地图)图层类,代码如下:
/// <summary>
/// bln格式图层(地图)
/// </summary>
public class BlnLayer : MapLayer, IEdit
{
/// <summary>
///构造方法
/// </summary>
/// <param name="source"></param>
public BlnLayer(string source)
: base(source)
{
}
/// <summary>
/// 重载读取数据抽象方法
/// </summary>
protected override void LoadData()
{
//读取数据
ReadFile(DataSource);
//如果图层名为noname,则将文件名作为图层名
if (LayerName.ToLower() == "noname") LayerName = Path.GetFileName(DataSource);
}
/// <summary>
/// 读取数据
/// </summary>
/// <param name="fn">文件名</param>
private void ReadFile(string fn)
{
using (FileStream fs = new FileStream(fn, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (StreamReader ws = new StreamReader(fs, Encoding.Default))
{
while (!ws.EndOfStream)
{
//数据行
string line = "";
//读取地图元素头,并跳过空行
do
{
line = ws.ReadLine();
}
while (string.IsNullOrEmpty(line));
//分隔数据行
string[] items = line.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
//点数
int n = Convert.ToInt16(items[0]);
//地图元素
MapModel mo = new MapModel();
//地图元素头为两个
if (items.Length > 1)
{
int t = Convert.ToInt16(items[1]);
if (t == 1)
{
//线
mo.ObjectType = ShapeType.PolyLine;
}
else
{
//面
mo.ObjectType = ShapeType.Polygon;
}
}
else
{
//线
mo.ObjectType = ShapeType.PolyLine;
}
//地图元素点数
mo.Points = new System.Drawing.PointF[n];
//读取经纬度
for (int i = 0; i < n; i++)
{
PointF ptf = new PointF();
line = "";
//读入一行,并跳过空行
do
{
line = ws.ReadLine();
}
while (string.IsNullOrEmpty(line));
//分隔数据行
items = line.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
//有两项时
if (items.Length == 2)
{
//经纬度
ptf.X = Convert.ToSingle(items[0]);
ptf.Y = Convert.ToSingle(items[1]);
mo.Points[i] = ptf;
}
}
//加入到列表
MapObjects.Add(mo);
}
}
fs.Close();
}
}
}
通过注释应该很容易看懂。代码主要在读数据上面,至于如何绘图的,不用管它,基类已经实现了。Bln格式地图效果如下:
三、完全控制绘图
上节看来,实现一种数据格式图层似乎很简单,选一个图层基类,重载它的读取数据方法就可以了。但是,FreeMicaps提供的图层基类有限,普通的地图、高空地面填图等有基类可继承,如果你想支持一种FreeMicaps没涉及到渲染方式,就没这么简单了。这就需要从CustomLayer图层继承,完全实现数据的读取和绘制。
前面说了,CustomLayer有两个抽象方法DoLoad()和DoRender(),子类实现它们以完成数据读取和渲染。读取数据不用说了,上节的例子已很清楚。绘图方法原型为:
/// <summary>
/// 渲染图层
/// </summary>
/// <param name="graphics">Graphics对象</param>
protected abstract void DoRender(Graphics graphics);
函数已传入Graphics对象,可以随心所欲在graphics上画线、画圆、输出字符串、贴图,但对于在地图上绘制,更多的是需要在指定地理坐标(经纬度)上绘制,Graphics对象绘图时传入的是屏幕坐标,这就需要把地理坐标转换为屏幕坐标。复习一下上一章,一开始就说的是坐标转换。CustomLayer有一个WeatherMap类的实例Map,利用它即可完成地理坐标到屏幕坐标的转换。
为了说明绘制,做个试验,在前面BLN格式图层上增加代码实现在指定经纬度画一个圆。给图层增加方法DoRender(),注意,DoRender()是虚方法,重载时要加override:
protected override void DoRender(Graphics graphics)
{
//基类绘图
base.DoRender(graphics);
//经纬度(110,35)
PointF ptf = new PointF(110, 35);
//转换为屏幕坐标
ptf = Map.WorldToScreen(ptf);
//画个半径为100的圆
int d = 100;
graphics.DrawEllipse(Pens.Red, new RectangleF(ptf.X - d, ptf.Y - d, d*2, d*2));
}
运行后效果如下:
虽然只是画了一个圈,但足能说明绘制思路,换成标注、图片已不是问题。还可以利用系统提供的符号绘制类画出风、天气现象、云等符号。
看到这里,写一个你需要的图层应该不是难事了。从CustomLayer继承一个子类,实现DoLoad()和DoRender()方法,DoLoad()里写读取数据代码,DoRender()里写绘图代码,绘制时使用Map对象进行经纬度到屏幕坐标转换。
图层开发基本方法就到这里,后面还将进一步继续讲解图层开发的高级功能。