第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)範圍下的顯示 

地圖縮放的背後原理就是這樣,不要求特定的距離單位都能直接繪製。這樣在以後涉及到圖層的概念時,如果兩個圖層的座標系統一致,你會發現可以很輕易的將兩個圖層疊加在一起,不會出現莫名的錯位問題。

此篇就是這些,還有很多更高級的功能及原理正在陸續趕來,看好關注,下期見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章