手寫地理信息組件系列 第3篇
Map座標變換的實現原理
難度指數:★★☆☆☆
目錄:
前情回顧
這一系列文章都是以由簡入深的方式展開的,上一章對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)範圍下的顯示
地圖縮放的背後原理就是這樣,不要求特定的距離單位都能直接繪製。這樣在以後涉及到圖層的概念時,如果兩個圖層的座標系統一致,你會發現可以很輕易的將兩個圖層疊加在一起,不會出現莫名的錯位問題。
此篇就是這些,還有很多更高級的功能及原理正在陸續趕來,看好關注,下期見!