前情回顧
上一篇是系列內容的第一篇。我們通過應用節點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小玩具”沒有太大的差別,但是其背後的實現,已經慢慢地開始變得有理有條。