第2篇. 坦率的讲,这很可能是一篇可将Geometry和你说清楚的文章

手写地理信息组件系列  第2篇
GIS基础对象的类体系
难度指数:★★☆☆☆

前情回顾

上一篇是系列内容的第一篇。我们通过应用节点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小玩具”没有太大的差别,但是其背后的实现,已经慢慢地开始变得有理有条。 


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值