接到一個任務,要把全國的公車資料座標訊息爬回來,並且做整理歸納。研究了一下交通部的公共運輸整合資訊平台 的內容,資訊相當完整,API也寫得很彈性。
唯獨公車站牌的部分,是以公車路線為出發點思考,每條路線雖然會經過同一個站牌,站牌的ID都會視為不同,不同ID的站牌,經緯度可能相同。
例如 : 富陽街口的公車站牌資料
[![](https://1.bp.blogspot.com/-rsJGRd8q0Yk/V6LuwauETRI/AAAAAAAAH2I/MXCywQPawicA3nbmx9j-D6608XWt5RxXQCLcB/s640/13765731_1287175481295263_7952967575366659415_o.jpg)](https://1.bp.blogspot.com/-rsJGRd8q0Yk/V6LuwauETRI/AAAAAAAAH2I/MXCywQPawicA3nbmx9j-D6608XWt5RxXQCLcB/s1600/13765731_1287175481295263_7952967575366659415_o.jpg)
富陽街口去程回程的公車,透過GoogleMap算大概有8支站牌,但透過API去撈會發現有17支ID皆為不同的資料。
任務目標 : 將所有站牌資料爬回來,相同的站名距離20公尺內的站牌要能自動聚合在一起,成為一支站牌。
原本思考是全部爬回來後用站名做整合,但發現一個問題是宜蘭與桃園都有相同站名的站牌”吊橋頭”,而距離相差十萬八千里,所以站名不可行。會加上20公尺的條件是因為像是台北火車站他的腹地很大,公車站牌可能很多相距很遠, 或是有些捷運站出口在兩條路口的轉角都有公車站牌,且站名相同,如果單純把這些點整合在一起放回地圖上,會發現公車站牌少了許多,失真。
歸納以上的條件,我們會有一大批公車站牌的資料(118728筆資料 ),如果把每個站牌用同名稱的逐筆下去比對各自的距離,顯然掃到天荒地老也跑不完,顯然浪費效能且不切實際。
看了一篇文章後得到非常大的啟發,該方法是透過四元樹演算法將大批座標快速塞選出範圍內的點,並且做出聚合。 參考 : How To Efficiently Display Large Amounts of Data on iOS Maps 四元樹演算法 : Wiki
節錄Wiki對四元樹的解說:「假如四元樹區塊被用來表達一組點資料(諸如一組城市的經緯度),區塊就進行次分割直到每個葉節點包含最多一個單點。 」
用圖片來表示就像這樣
[![](https://4.bp.blogspot.com/-waUqxqrmcf8/V6L7oy8YO-I/AAAAAAAAH2Y/kMcCy8J_kGw45_XPYsvcsqONAuQa767qgCLcB/s320/quadTreeInsert.gif)](https://4.bp.blogspot.com/-waUqxqrmcf8/V6L7oy8YO-I/AAAAAAAAH2Y/kMcCy8J_kGw45_XPYsvcsqONAuQa767qgCLcB/s1600/quadTreeInsert.gif) **Source : https://robots.thoughtbot.com/how-to-handle-large-amounts-of-data-on-maps **
四元樹包含幾個概念
1.每個節點最多只能包含一個座標
2.當節點的座標滿了,會分裂出四個象限
根據以上概念寫出以下Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class QuadTreeNode { public QuadTreeNode NorthWest { get ; set ; } public QuadTreeNode NorthEast { get ; set ; } public QuadTreeNode SouthWest { get ; set ; } public QuadTreeNode SouthEast { get ; set ; } public BoundingBoxDTO BoundingBox { get ; set ; } public int BucketCapacity { get ; set ; } public List<Data> Points { get ; set ; } public QuadTreeNode (BoundingBoxDTO boundingBox , int capacity ) { BoundingBox = boundingBox; BucketCapacity = capacity; Points = new List<BusStopAPIResult>(); } }
那座標插入四元樹節點時如何運作?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public bool Insert (QuadTreeNode node, Data point ) { if (!CheckBoundingBoxContainData(node.BoundingBox, point)) { return false ; } if ((node.Points.Count +1 ) <= node.BucketCapacity) { node.Points.Add(data); return true ; } if (node.NorthWest == null ) { Subdivide(node); } if (Insert(node.NorthWest, data)) return true ; if (Insert(node.NorthEast, data)) return true ; if (Insert(node.SouthWest, data)) return true ; if (Insert(node.SouthEast, data)) return true ; return false ; }
四元樹分裂的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 void Subdivide (QuadTreeNode node ) { var box = node.BoundingBox; double LatMid = (box.LeftTop.Latitude + box.RightBottom.Latitude) / 2.0 ; double LngMid = (box.LeftTop.Longitude + box.RightBottom.Longitude) / 2.0 ; var NorthWest = new BoundingBoxDTO { LeftTop = new CoordinateDTO { Latitude = box.LeftTop.Latitude, Longitude = box.LeftTop.Longitude }, RightBottom = new CoordinateDTO { Latitude = LatMid, Longitude = LngMid } }; node.NorthWest = new QuadTreeNode(NorthWest, node.BucketCapacity); var NorthEast = new BoundingBoxDTO { LeftTop = new CoordinateDTO { Latitude = box.LeftTop.Latitude, Longitude = LngMid }, RightBottom = new CoordinateDTO { Latitude = LatMid, Longitude = box.RightBottom.Longitude } }; node.NorthEast = new QuadTreeNode(NorthEast, node.BucketCapacity); var SouthWest = new BoundingBoxDTO { LeftTop = new CoordinateDTO { Latitude = LatMid, Longitude = box.LeftTop.Longitude }, RightBottom = new CoordinateDTO { Latitude = box.RightBottom.Latitude, Longitude = LngMid } }; node.SouthWest = new QuadTreeNode(SouthWest, node.BucketCapacity); var SouthEast = new BoundingBoxDTO { LeftTop = new CoordinateDTO { Latitude = LatMid, Longitude = LngMid }, RightBottom = new CoordinateDTO { Latitude = box.RightBottom.Latitude, Longitude = box.RightBottom.Longitude } }; node.SouthEast = new QuadTreeNode(SouthEast, node.BucketCapacity); }
看到這邊可能會有個疑問,那就是雖然知道四元樹就像是遞迴的概念,將地圖分裂成很多塊,每個大區塊可能又被分裂成若干的小區塊,一層一層的往下長
[![](https://1.bp.blogspot.com/-m_rkkVB1hxw/V6MCmq-X0gI/AAAAAAAAH2o/FHOE9VcWAvkLDp8BWJvjYnWvP9-NlbGfgCLcB/s320/500px-Point_quadtree.svg.png)](https://1.bp.blogspot.com/-m_rkkVB1hxw/V6MCmq-X0gI/AAAAAAAAH2o/FHOE9VcWAvkLDp8BWJvjYnWvP9-NlbGfgCLcB/s1600/500px-Point_quadtree.svg.png) **Source : https://zh.wikipedia.org/wiki/%E5%9B%9B%E5%8F%89%E6%A0%91 **
但為何會讓搜尋速度加快呢? 原因是他從上層開始看是否有重疊到要搜尋的區塊,如果有就往該節點的四象限遞迴繼續往下找重疊,如果在大區塊的節點就已經沒有包含重疊區域,那他無限往下延伸的子節點也就不會被搜尋了。引用原作者的圖片就一目了然
[![](https://3.bp.blogspot.com/-FXlbFONjFzI/V6MEINzRBHI/AAAAAAAAH20/gXhavItzQ_o3GgeyMPRgBGAxhJ9U6MuVwCLcB/s400/quadTreeQuery.gif)](https://3.bp.blogspot.com/-FXlbFONjFzI/V6MEINzRBHI/AAAAAAAAH20/gXhavItzQ_o3GgeyMPRgBGAxhJ9U6MuVwCLcB/s1600/quadTreeQuery.gif) **Source : https://robots.thoughtbot.com/how-to-handle-large-amounts-of-data-on-maps **
搜尋程式碼如下 如何透過座標判斷地理位置有無交集,請參考上一篇文章: 【地理位置】透過座標框出地理位置,與判斷地理位置是否重疊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void SearchDataInRange (QuadTreeNode node, BoundingBoxDTO range, ref List<Data> datas ) { if (!CheckIntersectionBoundingBox(node.BoundingBox, range)) { return ; } foreach (var item in node.Points) { if (CheckBoundingBoxContainData(range, item)) { datas.Add(item); } } if (node.NorthWest == null ) { return ; } SearchDataInRange(node.NorthWest, range, ref datas); SearchDataInRange(node.NorthEast, range, ref datas); SearchDataInRange(node.SouthWest, range, ref datas); SearchDataInRange(node.SouthEast, range, ref datas); }
說了這麼多,我們如何將上述的四元樹理論運用在我們的目標上呢?
首先我們把一堆的公車站牌資料抓回來後,依據名稱先進行第一次分類,把每個名稱相同的依據上面的方式做成四元樹節點,但這邊有個前面提到的問題,如果是同個站名但其實是不同的站點呢?
這邊我用的方法是將同名稱的公車站牌,找出兩個離最遠的站牌算距離,如果超過1公里合理懷疑這是兩個不同的站點,只是碰巧相同名稱。台灣應該沒有差了一公里遠但是是同一個站的吧……(有的話請跟我說XD)。
將這群同站名但其實不同地方的站牌,依據一公里為單位分組,這樣就能有效地將這類同名的站牌做更細部的歸類。
計算兩點座標距離的程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private const double EARTH_RADIUS = 6378.137 ; public static double GetDistance (double lat1, double lng1, double lat2, double lng2 ) { double radLat1 = rad(lat1); double radLat2 = rad(lat2); double a = radLat1 - radLat2; double b = rad(lng1) - rad(lng2); double s = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2 ), 2 ) + Math.Cos(radLat1) * Math.Cos(radLat2) * Math.Pow(Math.Sin(b / 2 ), 2 ))); s = s * EARTH_RADIUS; s = Math.Round(s * 10000 ) / 10 ; return s; } private static double rad (double d ) { return d * Math.PI / 180.0 ; }
上面的問題皆排除並分類後,依據同名的站牌,算出這群站牌座標的中心點,並用離中心最遠的座標距離做為範圍,依照每小格20公尺的矩形進行四元樹搜尋,在同一格內的整合成一個站牌,記錄下來
概念圖
[![](https://2.bp.blogspot.com/-C9CqLPtlx7Q/V6MKsEK1vTI/AAAAAAAAH3E/KDH_KTeXYQQnoFWCQdLL7mvIihxNMh7KgCLcB/s640/1.png)](https://2.bp.blogspot.com/-C9CqLPtlx7Q/V6MKsEK1vTI/AAAAAAAAH3E/KDH_KTeXYQQnoFWCQdLL7mvIihxNMh7KgCLcB/s1600/1.png)
1.這些點只是亂標的,但假設真的有個同名站牌坐落在這些位置,找到中心點後,藍色線為中心點與最遠座標的直線距離,依據這個距離做為矩形二分之一寬。
2.有了距離後就能求出左上跟右下角的點,透過DbGeography 這個來取得地理面積的物件 3.從左上開始,橘色的每小格為長寬各20公尺的矩形,透過四元樹下去搜尋,被同一格橘色小框框到的座標收納成同一個點,記錄下來
經過實測,如果只是雙北市的公車站點,只要4分鐘就能全部歸類掃描完畢,全國的話大概4個小時能算完,以上是這次任務的小筆記
測試環境 : CPU : I7 4核 (實際在跑的時候控制大概在3核左右) RAM : 16G