第3篇.坐标转换 | 看GIS如何把世界收入囊中!

手写地理信息组件系列 第3篇
Map坐标变换的实现原理
难度指数:★★☆☆☆

目录:

前情回顾

屏幕坐标与地图坐标

地图类的构造

涉及Geometry对象的重构

地理坐标实体的绘制

点绘制验证 


前情回顾

这一系列文章都是以由简入深的方式展开的,上一章对GIS当中基本的Geometry对象进行了一次更为清晰的重构,梳理了各对象间的继承组合关系。结构更体系了,调用也更方便。而这一篇将在之前的基础上扩展,进一步讨论关于空间对象的显示。由此你将会明确,空间对象显示的屏幕坐标地图坐标之间的转换,有哪些更为细节的问题。并着手实现一个可以地理坐标系显示的地图程序,一起动手吧。

屏幕坐标与地图坐标

我们面对的屏幕,不管是PC屏幕还是手机屏幕,本质是一系列像素点构成的二维矩阵,通常为矩形。现在显示设备的像素越来越高,几年前PC普遍是1366x768的,现在不到1920x1080像素的屏幕根本没人去买,而手机屏幕好多都高过1920x1080了,已经到了察觉不到像素点的程度,显示非常细腻。
这里说的像素点,其在整个二维矩阵中的位置就是像素坐标。例如在一个1920x1080的屏幕上,位于屏幕区最左上角的像素点坐标规定为(0,0),相应地,右下角的像素点位置为(1919,1079)。

 地图坐标可以表示地理空间的某个位置,常见用经纬度这种地理坐标来表示,同时也可以用投影坐标来表示,投影坐标由地理坐标投影后得来,一般单位为米。涉及到地理坐标和投影变换的知识,将在以后专门介绍。此篇中的地图坐标可视为投影坐标。

地图类的构造

地图(Map)可以理解为观察世界的一个窗口,这个窗口内的世界范围是可变的。通过固定地图窗口的大小,调节地理范围,形成地图显示效果的变化,也就是常说的地图缩放(zoom)

实现地图的缩放,需要计算比例尺(scale),形成地理坐标和像素坐标的对应关系。继而进行两种坐标之间的转换。比例尺的数值由两坐标系各自形成的距离比值得来。

下面来实现屏幕坐标和地图坐标的转换。

Map类是地图显示中最常用的类,后续更高级的显示功能都将在此基础上展开。

    public class Map
    {
        //地图范围
        Extent mapExtent;

        //地图窗口范围
        Rectangle rectangle;

        //横纵轴比例尺(实际Size/窗口Size)
        double scaleX, scaleY;

        public Map()
        {
            //无参构造函数,仅做内部变量初始化
            Update(new Extent(new Vertex(300, 0), new Vertex(0, 300)),//左下、右上组成一个范围
              new Rectangle(0, 0, 100, 100));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="mapExtent">地图范围</param>
        /// <param name="rectangle">地图窗口范围</param>
        public void Update(Extent mapExtent, Rectangle rectangle)
        {
            this.mapExtent = mapExtent;
            this.rectangle = rectangle;
            scaleX = this.mapExtent.Width / this.rectangle.Width;
            scaleY = this.mapExtent.Height / this.rectangle.Height;
        }

        public System.Drawing.Point ToScreenPoint(Vertex vertex)
        {
            double x = (vertex.x - mapExtent.MinX) / scaleX;

            //屏幕坐标Y轴向下,地图坐标Y轴向上,请读者体会这里的算法
            double y = this.rectangle.Height - (vertex.y - mapExtent.MinY) / scaleY;
            return new System.Drawing.Point((int)x, (int)y);
        }

        public Vertex ToMapVertex(System.Drawing.Point point)
        {
            double x = point.X * scaleX;
            double y = point.Y * scaleY;
            return new Vertex(x, y);
        }
    }

    public class Extent
    {
        //左下
        Vertex bottomLeft;

        //右上
        Vertex upRight;

        public double MinX
        {
            get { return bottomLeft.x; }
        }

        public double MinY
        {
            get { return bottomLeft.y; }
        }

        public double MaxX
        {
            get { return upRight.x; }
        }

        public double MaxY
        {
            get { return upRight.y; }
        }

        public double Width
        {
            get { return upRight.x - bottomLeft.x; }
        }

        public double Height
        {
            get { return upRight.y - bottomLeft.y; }
        }

        public Extent(Vertex bottomleft, Vertex upright)
        {
            this.bottomLeft = bottomleft;
            this.upRight = upright;
        }
    }

涉及Geometry对象的重构

在之前的Geometry系列对象中,坐标都是以屏幕坐标作为表示的,其绘制也直接以屏幕坐标绘制。引入地图坐标概念后,坐标必须需要经过转换后才能正确绘制,需要对以下对象进行改动。

    class Point : Geometry
    {
        public Point(Vertex vertex)
        {
            centroid = vertex;
            extent = new Extent(vertex, vertex);
        }

        public double Distance(Vertex another)
        {
            return centroid.Distance(another);
        }

        //新增map参数
        public override void Draw(Graphics graphics, Map map)
        {
            //增加地图坐标到屏幕坐标的转换
            System.Drawing.Point point = map.ToScreenPoint(centroid);

            graphics.FillEllipse(new SolidBrush(Color.Red),
              new Rectangle(point.X, point.Y, 5, 5));
        }
    }

    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, Map map);
    }

对属性绘制方法的改动:

    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)
        {
            graphics.DrawString(table[key].ToString(), new Font("宋体", 20),
                new SolidBrush(Color.Blue), new PointF((int)location.x, (int)location.y));
        }

        //增加map参数及坐标转换
        public void Draw(Graphics graphics, Map map, Vertex location, string key)
        {
            System.Drawing.Point point = map.ToScreenPoint(location);
            //graphics.DrawString
            string name = table[key].ToString();
            graphics.DrawString(name,
               new Font("宋体", 20),
               new SolidBrush(Color.Blue),
               new PointF(point.X, point.Y));
        }
    }

对要素绘制方法的改动:

    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, Map map, string fieldName)
        {
            geometry.Draw(graphics, map);
            attribute.Draw(graphics, map, geometry.Centroid, fieldName);
        }

        public Geometry GetGeometry()
        {
            return geometry;
        }

        public Object GetAttributeValue(String fieldName)
        {
            return attribute.GetValue(fieldName);
        }
    }

通过对以上对象的观察,可以发现主要是增加了坐标转换的步骤,未对各对象的实质功能做出改变。这里省却了各对象未修改的部分,具体可以参考上一篇,查看完整定义。 

地理坐标实体的绘制

设计界面:

界面增加了地图坐标的对象绘制。“添加”分组框的XY为地理实体点的地理坐标输入框,“地理范围”分组框的四个参数,定义了当前地图窗口的地理范围,至于地图窗口的Size,取界面的Rectangle作其范围。

 

    public partial class FormThirdPart : Form
    {
        Map map;
        Extent mapExtent;
        List<Feature> features = new List<Feature>();
        public FormThirdPart()
        {
            InitializeComponent();
            map = new Map();
            btn_MapUpdate_Click(null, null);
        }


        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(), map, textBox_Attr_Name.Text);

            features.Add(feature);
        }

        private void btn_MapUpdate_Click(object sender, EventArgs e)
        {
            double minX = Double.Parse(txtBox_minX.Text);
            double minY = Double.Parse(txtBox_minY.Text);
            double maxX = Double.Parse(txtBox_maxX.Text);
            double maxY = Double.Parse(txtBox_maxY.Text);

            mapExtent = new Extent(
              new Vertex(minX, minY), new Vertex(maxX, maxY));

            //重绘
            mapUpdate();
        }
        //距离容限值 10 像素
        const int Tolerance = 10;
        private void FormThirdPart_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();
                }
            }
        }

        //更新界面--重绘
        private void mapUpdate()
        {
            map.Update(mapExtent, this.ClientRectangle);

            Graphics graphic = this.CreateGraphics();
            //清理界面,准备重绘
            graphic.Clear(this.BackColor);
            foreach (Feature f in features)
            {
                f.Draw(graphic, map, textBox_Attr_Name.Text);
            }
            graphic.Dispose();
        }

        private void FormThirdPart_Paint(object sender, PaintEventArgs e)
        {
            mapUpdate();
        }


    }

点绘制验证 

此之前,窗口内可显示的点坐标XY值是不会超出窗口的长宽范围的,现准备的点坐标为(2000,2000),显然连显示器的显示范围都已经超出了,但是通过设置一个更大的地理范围(0,0)(4000,4000),该点仍能显示在地图窗口之中。

如果对单个点绘制效果感觉不强,做一次多点的测试可能会更加直观。

点1. (100,100)
点2. (100,200)
点3. (300,300)

在(0,0)(600,600)范围下的显示 

在(0,0)(1000,1000)范围下的显示 

地图缩放的背后原理就是这样,不要求特定的距离单位都能直接绘制。这样在以后涉及到图层的概念时,如果两个图层的坐标系统一致,你会发现可以很轻易的将两个图层叠加在一起,不会出现莫名的错位问题。

此篇就是这些,还有很多更高级的功能及原理正在陆续赶来,看好关注,下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值