在博客園學習一段時間的C#了,一直想寫點東西作為回報,可惜自己懂得少,能寫出來的大部分網上都能搜到現成。正好這段時間幫同事寫了個地圖離散點找外接凸多邊形的實現,網上大概搜了下好像沒有,不如分享出來大家一起看看。
先說下基本邏輯。1,找到所有點集中緯度最高的點作為多邊形的起點。2,遍歷其余點找到第二個點,使得除這兩點外的其他所有點都在這兩點連線的同一側。3,繼續尋找第三個,第四個點,直到找到的某一個點就是最北邊的點,多邊形閉環形成。
需要注意的地方是,第一個和第二個點簡單,從第三個點開始需要判斷找到的點不能已經存在於列表中。對照圖片可能說的更清楚一些,如圖a,b,c依次為多邊形頂點。僅判斷所有點在同一側的話從b點會找到a和c都滿足,因此需要判斷找到的點不能是上一個找到的點。下面代碼處會寫說明。
先定義坐標點類,主要成員經度,緯度。另外重載相等和不等運算符,以及Equals和GetHashCode方法。
class Location { private double longitude; private double latitude; public Location(double _longitude, double _latitude) { longitude = _longitude; latitude = _latitude; } /// <summary> /// 經度 /// </summary> public double Longitude { get { return longitude; } set { longitude = value; } } /// <summary> /// 緯度 /// </summary> public double Latitude { get { return latitude; } set { latitude = value; } } public static bool operator ==(Location lhs, Location rhs) { return lhs.Latitude == rhs.Latitude && lhs.Longitude == rhs.Longitude; } public static bool operator !=(Location lhs, Location rhs) { return !(lhs == rhs); } public override bool Equals(object obj) { if (obj == null) { return false; } if ((obj.GetType().Equals(this.GetType())) == false) { return false; } Location temp = null; temp = (Location)obj; return this.Latitude.Equals(temp.Latitude) && this.Longitude.Equals(temp.Longitude); } public override int GetHashCode() { return this.Latitude.GetHashCode() + this.Longitude.GetHashCode(); } }
再定義FindPolygon類,並用坐標點集合list來初始化對象。
class FindPolygon { private List<Location> list; /// <summary> /// 用點集list初始化對象 /// </summary> /// <param name="_list"></param> public FindPolygon(List<Location> _list) { list = _list; } }
先寫出來找點的上層邏輯,底層再根據需要逐步完成。從最北邊的點開始找下一個點,直到找到下一個點又回到最北邊的點形成閉環為止。
/// <summary> /// 從最北邊的點開始找下一個點,使其余所有點在這個點和下個點的連線的同一側 /// </summary> /// <returns>外接多邊形頂點集合</returns> public List<Location> GetPolygon() { List<Location> result = new List<Location>(); //最北邊的點作為多邊形第一個點 Location a = NorthernMost(); Location b = NorthernMost(); result.Add(b);//add第一個點 Location c = GetNextPoint(b, b);//第一次找點特殊處理 while (!NorthernMost().Equals(c)) { a = b; b = c;//交換abc三個點,繼續找下一個點 result.Add(c); c = GetNextPoint(b, a); } return result; }
寫完上一段代碼,發現 NorthernMost()和 GetNextPoint() 方法需要繼續完成,這里就需要判斷根據第一個點找第二個點,但找到的不能是第〇個點了。
/// <summary> /// 找到最北邊的點為多邊形的第一個點 /// </summary> /// <returns></returns> private Location NorthernMost() { Location lo = list[1]; foreach (var l in list) { if (l.Latitude > lo.Latitude) lo = l; } return lo; } /// <summary> /// 給定一個點獲取另一個點,使得列表中其他點都在兩點連線同一側,但是找到的這個點不能是上一個點 /// </summary> /// <param name="_currentpoint">給定這個點</param> /// <param name="_lastpoint">上一個點</param> /// <returns></returns> private Location GetNextPoint(Location _currentpoint, Location _lastpoint) { foreach (Location l in list) { if (!_currentpoint.Equals(l)) { if (AtOneSide(_currentpoint, l) && !_lastpoint.Equals(l)) return l; } } return null;//這個地方是走不到的 }
繼續寫完上述代碼,發現AtOneSide()方法需要實現。於是繼續,根據給定的兩點判斷其余點是否都在兩點連線的同一側。首先找到任意一個第三點,根據直線方程y=kx+b判斷他在給定兩點連線的哪一側,再判斷其余點是否都在同一側,如果不是該點就不是我們要找的點。
/// <summary> /// 判斷所有點是否在兩點連線一側 /// </summary> /// <param name="l1"></param> /// <param name="l2"></param> /// <returns></returns> private bool AtOneSide(Location l1, Location l2) { double k = (l1.Latitude - l2.Latitude) / (l1.Longitude - l2.Longitude); double b = l1.Latitude - l1.Longitude * (l1.Latitude - l2.Latitude) / (l1.Longitude - l2.Longitude); bool v = GetV(l1, l2, k, b); foreach (Location l in list) { if (!l.Equals(l1) && !l.Equals(l2)) { if ((l.Latitude > (k * l.Longitude + b)) != v) return false; } } return true; } /// <summary> /// 點集中隨便找到第三點,確定他在兩點連線的哪一側 /// </summary> /// <param name="l1"></param> /// <param name="l2"></param> /// <param name="k"></param> /// <param name="b"></param> /// <returns></returns> private bool GetV(Location l1, Location l2, double k, double b) { foreach (Location l in list) { if (!l.Equals(l1) && !l.Equals(l2)) { return l.Latitude > (k * l.Longitude + b); } } return false;//這個地方是走不到的 }
到這里就全部完成了,里面用到了中學數學知識直線方程。
遺留問題:本次處理的全部坐標為國內坐標,沒有跨半球的情況。如果需要處理跨半球坐標需要將本初子午線和赤道的焦點視為平面直角坐標的原點進行坐標象限轉換。
最后貼出完整代碼

namespace ns { class FindPolygon { private List<Location> list; /// <summary> /// 用點集list初始化對象 /// </summary> /// <param name="_list"></param> public FindPolygon(List<Location> _list) { list = _list; } /// <summary> /// 從最北邊的點開始找下一個點,使其余所有點在這個點和下個點的連線的同一側 /// </summary> /// <returns>外接多邊形頂點集合</returns> public List<Location> GetPolygon() { List<Location> result = new List<Location>(); //最北邊的點作為多邊形第一個點 Location a = NorthernMost(); Location b = NorthernMost(); result.Add(b);//add第一個點 Location c = GetNextPoint(b, b);//第一次找點特殊處理 while (!NorthernMost().Equals(c)) { a = b; b = c;//交換abc三個點,繼續找下一個點 result.Add(c); c = GetNextPoint(b, a); } return result; } /// <summary> /// 找到最北邊的點為多邊形的第一個點 /// </summary> /// <returns></returns> private Location NorthernMost() { Location lo = list[1]; foreach (var l in list) { if (l.Latitude > lo.Latitude) lo = l; } return lo; } /// <summary> /// 給定一個點獲取另一個點,使得列表中其他點都在兩點連線同一側,但是找到的這個點不能是上一個點 /// </summary> /// <param name="_currentpoint">給定這個點</param> /// <param name="_lastpoint">上一個點</param> /// <returns></returns> private Location GetNextPoint(Location _currentpoint, Location _lastpoint) { foreach (Location l in list) { if (!_currentpoint.Equals(l)) { if (AtOneSide(_currentpoint, l) && !_lastpoint.Equals(l)) return l; } } return null; } /// <summary> /// 判斷所有點是否在兩點連線一側 /// </summary> /// <param name="l1"></param> /// <param name="l2"></param> /// <returns></returns> private bool AtOneSide(Location l1, Location l2) { double k = (l1.Latitude - l2.Latitude) / (l1.Longitude - l2.Longitude); double b = l1.Latitude - l1.Longitude * (l1.Latitude - l2.Latitude) / (l1.Longitude - l2.Longitude); bool v = GetV(l1, l2, k, b); foreach (Location l in list) { if (!l.Equals(l1) && !l.Equals(l2)) { if ((l.Latitude > (k * l.Longitude + b)) != v) return false; } } return true; } /// <summary> /// 點集中隨便找到第三點,確定他在兩點連線的哪一側 /// </summary> /// <param name="l1"></param> /// <param name="l2"></param> /// <param name="k"></param> /// <param name="b"></param> /// <returns></returns> private bool GetV(Location l1, Location l2, double k, double b) { foreach (Location l in list) { if (!l.Equals(l1) && !l.Equals(l2)) { return l.Latitude > (k * l.Longitude + b); } } return false;//這個地方是走不到的 } } class Location { private double longitude; private double latitude; public Location(double _longitude, double _latitude) { longitude = _longitude; latitude = _latitude; } /// <summary> /// 經度 /// </summary> public double Longitude { get { return longitude; } set { longitude = value; } } /// <summary> /// 緯度 /// </summary> public double Latitude { get { return latitude; } set { latitude = value; } } public static bool operator ==(Location lhs, Location rhs) { return lhs.Latitude == rhs.Latitude && lhs.Longitude == rhs.Longitude; } public static bool operator !=(Location lhs, Location rhs) { return !(lhs == rhs); } public override bool Equals(object obj) { if (obj == null) { return false; } if ((obj.GetType().Equals(this.GetType())) == false) { return false; } Location temp = null; temp = (Location)obj; return this.Latitude.Equals(temp.Latitude) && this.Longitude.Equals(temp.Longitude); } public override int GetHashCode() { return this.Latitude.GetHashCode() + this.Longitude.GetHashCode(); } } }