前情回顾
上一篇是系列内容的第一篇。我们通过应用节点Vertex构建出了基本的空间对象点线面,并用这些对象构造了一个简单的GIS小玩具玩了起来。这一篇将在前篇的基础上,系统化GIS的基础对象。并应用这种更完善的对象体系,再次构建这个简单的地图程序,体会其应用的便捷之处。
空间图形的抽象
点线面虽然各自表示不同的内容,但是在本质上都属于同一种概念,就是图形学中的几何(Geometry)。
我们经常可以看到Geometry这个名词,为什么要对点线面做抽象呢,其实道理也简单,和面向对象设计的要求基本一致,那就是复用。优点是可以很优雅的将子类的共同特征收集起来,用以简化子类的设计。
回到点线面的共同特征,我们现在找到了两个,那就是位置和范围。位置的表示很好办,用一个点(Vertex)就能表示,这里我们用图形的质心(centroid)来表示图形的位置。但是点线面的范围大小如何描述?它们的形状可以各不相同,用哪种结构可以表达这种概念?
想必小伙伴已经可以猜到,那就是Extent。这个概念也算是很常见的了,但是各商业软件或者开源组件对它的表达词汇有所不同,有叫Bounds的,也有叫Boundary、BoundingBox或者MBR的,本质都是这个概念。我更倾向于叫它外接矩形,这个词汇对我来说更形象,也好理解。
点是没有面积的,自然它的Extent就是它本身。线的Extent就是以折线的两个结点(node)为对角线形成的矩形。而多边形的Extent就是构成多边形一系列节点中,最小与最大坐标围成的矩形。
值得注意的是,严谨的讲,面的外接矩形与其最小外接矩形,是两种经常容易被混淆的概念。外接矩形通常是指一个平行于两个坐标轴的矩形,而最小外接矩形(SMBR)不一定平行于坐标轴,但它的面积应该是所有外接矩形中最小的。如果不严格区分,两个词都代表平行于坐标轴的那一个。
将以上概念用代码形式做个表示,这里以Geometry抽象类的形式抽象几何图形,以实现复用。
public abstract class Geometry
{
//质心点
protected Vertex centroid;
//外接矩形
protected Extent extent;
//为了类的安全性,访问器只设置get,一经初始化,不可由非子类更改
public Vertex Centroid
{
get { return centroid; }
}
public Extent Extent
{
get { return extent; }
}
public abstract void Draw(Graphics graphics);
}
//外接矩形
public class Extent
{
//左下
public Vertex bottomLeft;
//右上
public Vertex upRight;
public Extent(Vertex bottomleft, Vertex upright)
{
this.bottomLeft = bottomleft;
this.upRight = upright;
}
}
细心的小伙伴可能注意到,C#中的set get访问器,抽象类中只用到了一个。这里的目的是防止由非子类对象对其进行修改。可以设想一下,如果我们实例化了一个线对象,线对象的两个端点坐标已经确定,这个时候我们对中点或者外接矩形坐标进行重新赋值,势必造成这几种坐标逻辑上不一致的问题,所以centroid和extent要在子类创建的时刻就确定下来,不给别人中途改变它的机会。
点线面子类
点线面子类由Geometry父类继承而来,继承了父类属性和抽象方法,在其实例化时为父类赋值,这样外部对象就可以直接访问父、子对象开放的成员方法及变量,实现客户端逻辑。以下只实现了点对象的内容,线面对象的实现将在以后的系列补充。
class Point : Geometry
{
public Point(Vertex vertex)
{
//为父类属性赋值
centroid = vertex;
extent = new Extent(vertex, vertex);
}
//计算两点距离
public double Distance(Vertex another)
{
return centroid.Distance(another);
}
//绘制
public override void Draw(Graphics graphics)
{
graphics.FillEllipse(new SolidBrush(Color.Red),
new Rectangle((int)Centroid.x, (int)Centroid.y, 5, 5));
}
}
//线实体
class Line : Geometry
{
List<Vertex> vertexs;
public override void Draw(Graphics graphics)
{
}
}
//面实体
class Polygon : Geometry
{
List<Vertex> vertexs;
public override void Draw(Graphics graphics)
{
}
}
空间实体的属性
空间实体的属性在上一篇中已经有所涉及,当时是将属性与图形封装在一个对象当中的,现在需要将其分拆出来,分别表示。将属性分拆为Attribute类表示,并用键值对存储。
class Attribute
{
//C#中用以存储键值对的一种容器类,这里用来存储字段名和字段值
private Hashtable table = new Hashtable();
public void AddValue(string fieldName, Object value)
{
table.Add(fieldName, value);
}
public Object GetValue(string fieldName)
{
return table[fieldName];
}
//界面绘制字段值
public void Draw(Graphics graphics, Vertex location, string key)
{
string val =table[key].ToString();
graphics.DrawString(val, new Font("宋体", 20),
new SolidBrush(Color.Blue), new PointF((int)location.x, (int)location.y));
}
}
空间对象的完整描述-要素
描述现实世界的一个对象,不仅需要描述其位置、大小等几何要素,还要结合其属性进行完整的描述,例如一个城市可以用多边形来描述其空间,再用GDP、人口等指标描述其属性,两者组合起来,就构成了一个要素(Feature)。简单来说Geometry+Attribute构成了Feature。
class Feature
{
private Geometry geometry;
private Attribute attribute;
public Feature(Geometry geometry, Attribute attribute)
{
this.geometry = geometry;
this.attribute = attribute;
}
public void Draw(Graphics graphics, string fieldName)
{
geometry.Draw(graphics);
attribute.Draw(graphics, geometry.Centroid, fieldName);
}
public Geometry GetGeometry()
{
return geometry;
}
public Object GetAttributeValue(String fieldName)
{
return attribute.GetValue(fieldName);
}
}
几何-属性-要素这三种概念已经分别实现出来,形成以下类图。
类图中,空心箭头代表继承关系;短线箭头代表组合关系。以上通过对空间对象的分解和组合,相信在你的头脑里,已经形成了空间对象的这一套概念模型。
照例,仍然将设计界面抛出来,梳理调用逻辑。
“添加”分组框中的X,Y照例是输入图形的坐标,Name和Value框是待添加图形的属性名和属性值。
“查询”分组框中的显示字段输入框(DisplayField),用以设置鼠标点选图形时,显示图形的哪个属性。
以下实现:注册按钮点击事件,生成要素,保存要素集合。
List<Feature> features = new List<Feature>();
private void BtnAddPoint_Click(object sender, EventArgs e)
{
double x = Convert.ToDouble(textBox_X.Text);
double y = Convert.ToDouble(textBox_Y.Text);
//构造图形
Vertex vertex = new Vertex(x, y);
GisClass.Point p = new GisClass.Point(vertex);
//构造属性
GisClass.Attribute attr = new GisClass.Attribute();
attr.AddValue(textBox_Attr_Name.Text, textBox_Attr_Value.Text);
//构造要素
Feature feature = new Feature(p, attr);
feature.Draw(this.CreateGraphics(), textBox_Attr_Name.Text);
features.Add(feature);
}
同样,注册窗体鼠标点击事件,在窗体控件点击时执行要素查询,弹出属性值。这与之前的查询逻辑基本一致。
//距离容限值 10 像素
const int Tolerance = 10;
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
Vertex vertex = new Vertex(e.X, e.Y);
double minDistance = Double.MaxValue;
Feature nearest = null;
//筛选与鼠标点选位置最近的坐标点
foreach (Feature f in features)
{
double distance =
f.GetGeometry().Centroid.Distance(vertex);
if (distance < minDistance)
{
nearest = f;
minDistance = distance;
}
}
if (nearest != null && nearest.GetGeometry().Centroid.Distance(vertex) < Tolerance)
{
MessageBox.Show(
nearest.GetAttributeValue(textBox_DisplayField.Text).ToString());
}
else
{
textBox_X.Text = e.X.ToString();
textBox_Y.Text = e.Y.ToString();
}
}
现在可以做交互层面最后的调用了。这里输入了三个点坐标,属性名均为city,属性值分别是南京,黄山和武汉,下边的查询字段也写上了city,点击了武汉点附近,乖乖的弹出了属性值。
窗体缩小后再还原时,会发现图形不见了,需要给窗体或者承载图形的控件增加重绘
初始化窗体时注册事件:
this.Paint += new System.Windows.Forms.PaintEventHandler(this.FormSecondPart_Paint);
private void FormSecondPart_Paint(object sender, PaintEventArgs e)
{
mapUpdate();
}
private void mapUpdate()
{
Graphics graphic = this.CreateGraphics();
//清理界面,准备重绘
graphic.Clear(this.BackColor);
foreach (Feature f in features)
{
f.Draw(graphic, textBox_DisplayField.Text);
}
graphic.Dispose();
}
从界面操作看来,虽然与“GIS小玩具”没有太大的差别,但是其背后的实现,已经慢慢地开始变得有理有条。