地鐵線路圖的設計與實現
在北京、上海這樣的一線城市,地鐵絕對是上班族的首選交通工具,儘管有時擠得要命,但你真的找不出比地鐵更準點的交通工具了。平時出門,我也總是習慣於在百度地圖或丁丁地圖裡先查詢一下地鐵乘車路線,這些程式用起來非常方便。最近幾天終於有點空餘時間了,我就在想,我是否也可以寫一個這樣的程式?作為一名專業碼農,我決定立刻動手。
首先,我給地鐵線路圖程式MetroGraphApp設定了幾個關鍵目標:
1、 操作介面模仿百度地圖,可以直接線上路圖上設定起點和終點。
2、 路徑查詢演算法不能太慢,絕大多數情況下,必須小於1秒。
3、 線路圖資料必須是可配置的,適用於各個城市的地鐵。
在介紹實現方法之前,先看一下最終的效果圖:
圖 1
圖 2
【技術準備】
MetroGraphApp是一個.NET WinForm程式,開發工具是VS2010,開發語言是C#,繪圖功能基於GDI+。線路圖資料儲存在一個XML檔案中。查詢路線時採用的是資料結構中的最短路徑法。
【總體設計】
地鐵線路構成了資料結構中的圖(Graph),站點是節點(Node),站點之間的通行路徑就是連結(Link),也就是邊(Edge)。由於所有通行路徑都是雙向的,所以,這是一個無向圖。在圖中任選兩點,兩點之間的連續路徑(Path)就是我們要查詢的乘車路線。
圖 3
在實際的地鐵線路中,每條Link都屬於一條線路(Line),例如:1號線、2號線,等等。兩個站點之間可能有兩條Link,它們分別屬於不同的Line,這就是“雙線並軌”。例如,在上海地鐵線路中,“寶山路”到“宜山路”這段路徑,“3號線”和“4號線”是在同一條軌道上執行的。如果是雙線並軌,繪圖的時候要並行繪製在一起。考慮到實用性和複雜性,我們在這裡忽略多線並軌的情況。
整體類設計如下圖所示:
圖 4
MetroGraphView類是一個自定義的UserControl,用於繪製MetroGraph所表示的地鐵線路圖。預設情況下是沒有任何資料的,所需的線路資料是通過LoadFromFile來裝載的,該方法可以從指定的XML檔案中讀取資料。
在MetroGraphView控制元件上,使用者可以通過滑鼠點選的方式,選擇起點和終點,然後控制元件會自動呼叫FindPath方法,獲取兩點之間的乘車路線(MetroPath),並將其繪製出來。
【原始碼實現】
MetroGraph、MetroNode、MetroLink、MetroLine、MetroPath類的實現都非常簡單,這裡不做過多解釋,感興趣的同學可以在文章末尾處下載原始碼。
接下來,我們將重點介紹兩個關鍵功能的實現:
1、 如何繪製線路圖?
2、 如何查詢乘車路線?
【如何繪製線路圖】
地鐵線路圖的繪製可以分解為兩個部分:節點繪製和連結繪製。
/// <summary> /// 繪製地鐵線路圖。 /// </summary> /// <param name="g">繪圖圖面。</param> /// <param name="graph">地鐵線路圖。</param> private void PaintGraph(Graphics g, MetroGraph graph) { //繪製地鐵路徑 foreach (var link in graph.Links.Where(c => c.Flag >= 0)) PaintLink(g, link); //繪製地鐵站點 foreach (var node in graph.Nodes) PaintNode(g, node); }
細心的讀者會發現,繪製Link的時候,有一個c.Flag>=0的篩選條件,這個Flag是幹什麼的呢?我來解釋一下。
由於地鐵的行車路徑都是雙向的,所以,我們在構造MetroGraph的時候,兩個Node之間的Link一定是成對出現的,這兩條Link的方向是相反的。但是在繪圖的時候,我們只需要繪製其中的一條即可。這裡就有一個邏輯問題,當繪製一條Link的時候,如何判斷其反向Link已經繪製過了?我用了一個最簡單的辦法,直接在Link上放一個標誌Flag,如果Flag=0,則繪製,如果Flag=-1,則不繪製。這個Flag是在構造XML資料的時候直接填進去的。
此外,Flag還有另外一個重要用途。文章前面提到過“雙線並軌”的問題,例如圖3中的A、B兩個節點,他們之間存在Line3和Line4並軌的現象。對於並軌的兩條Link,我們需要將其畫成兩條平行線,這兩條線可能是水平線、垂直線,也可能是斜線。線之間沒有空隙。如下圖所示:
圖 5
如何繪製這樣兩條平行線呢?辦法很簡單,只要將線段分別向兩邊移動一定距離即可(Flag的值可以控制移動方向)。假如Link的寬度是5px,那麼移動的距離應該是 2.5px,由於DrawLine時用的Pen預設是居中對齊的,這樣就可以畫出沒有間隙的兩條平行線。程式碼如下:
/// <summary> /// 繪製地鐵站點間的線路。 /// </summary> /// <param name="g">繪圖圖面。</param> /// <param name="link">地鐵站點間的線路。</param> private void PaintLink(Graphics g, MetroLink link) { Point pt1 = new Point(link.From.X, link.From.Y); Point pt2 = new Point(link.To.X, link.To.Y); using (Pen pen = new Pen(link.Line.Color, 5)) { pen.LineJoin = LineJoin.Round; if (link.Flag == 0) {//單線 g.DrawLine(pen, pt1, pt2); } else if (link.Flag > 0) {//雙線並軌(如果是同向,則Flag分別為1和2,否則都為1) float scale = (pen.Width / 2) / Distance(pt1, pt2); float angle = (float)(Math.PI / 2); if (link.Flag == 2) angle *= -1; //平移線段 var pt3 = Rotate(pt2, pt1, angle, scale); var pt4 = Rotate(pt1, pt2, -angle, scale); g.DrawLine(pen, pt3, pt4); } } }
節點的繪製就要簡單多了。節點由圓圈和標籤構成,圓圈在上,標籤在下。標籤的位置,是個值得改進的問題,因為標籤可能會把Link線條給蓋住。在本程式中,我認為影響不是很大,所以,我把標籤統一放在圓圈的下方。
此外,對於可以換乘不同Line的站點,我們需要把圓圈畫得大一些,這樣更醒目。程式碼如下:
/// <summary> /// 繪製地鐵站點。 /// </summary> /// <param name="g">繪圖圖面。</param> /// <param name="node">地鐵站點。</param> private void PaintNode(Graphics g, MetroNode node) { //繪製站點圓圈 Color color = node.Links.Count > 2 ? Color.Black : node.Links[0].Line.Color; var rect = GetNodeRect(node); g.FillEllipse(Brushes.White, rect); using (Pen pen = new Pen(color)) { g.DrawEllipse(pen, rect); } //繪製站點名稱 var sz = g.MeasureString(node.Name, this.Font).ToSize(); Point pt = new Point(node.X - sz.Width / 2, node.Y + (rect.Height >> 1) + 4); g.DrawString(node.Name, Font, Brushes.Black, pt); }
【如何查詢乘車路線】
這是圖論中典型的路徑搜尋問題。當我處理這個問題的時候,我首先想到並實現的是最短路徑法。最短路徑法具有很強的現實意義,它表明路線比較節省時間。判斷時間長短的辦法由兩個:一是通過累加Link上的權重(Weight)來判斷,二是通過Link數量來判斷。本文程式採用的是後者,因為根據實際經驗,各個站點之間的執行時間是差不多的,以上海地鐵為例,平均時間是大概3分鐘一站。當然,我們沒有考慮換乘的時間。程式碼如下:
/// <summary> /// 查詢指定兩個節點之間的最短路徑。 /// </summary> /// <param name="startNode">開始節點。</param> /// <param name="endNode">結束節點。</param> /// <param name="line">目標線路(為null表示不限制線路)。</param> /// <returns>乘車路線列表。</returns> private List<MetroPath> FindShortestPaths(MetroNode startNode, MetroNode endNode, MetroLine line) { List<MetroPath> pathtList = new List<MetroPath>(); if (startNode == endNode) return pathtList; //路徑佇列,用於遍歷路徑 Queue<MetroPath> pathQueue = new Queue<MetroPath>(); pathQueue.Enqueue(new MetroPath()); while (pathQueue.Count > 0) { var path = pathQueue.Dequeue(); //如果已經超過最短路徑,則直接返回 if (pathtList.Count > 0 && path.Links.Count > pathtList[0].Links.Count) continue; //路徑的最後一個節點 MetroNode prevNode = path.Links.Count > 0 ? path.Links[path.Links.Count - 1].From : null; MetroNode lastNode = path.Links.Count > 0 ? path.Links[path.Links.Count - 1].To : startNode; //繼續尋找後續節點 foreach (var link in lastNode.Links.Where(c => c.To != prevNode && (line == null || c.Line == line))) { if (link.To == endNode) { MetroPath newPath = path.Append(link); if (pathtList.Count == 0 || newPath.Links.Count == pathtList[0].Links.Count) {//找到一條路徑 pathtList.Add(newPath); } else if (newPath.Links.Count < pathtList[0].Links.Count) {//找到一條更短的路徑 pathtList.Clear(); pathtList.Add(newPath); } else break;//更長的路徑沒有意義 } else if (!path.ContainsNode(link.To)) { pathQueue.Enqueue(path.Append(link)); } } } return pathtList; }
上述演算法在大多數情況下都執行得很好,但是存在兩個不足之處:
1、“較少換乘”這個優先性沒有體現出來。程式給出的最短路徑,往往有換乘1站甚至2站,而事實上有一條直達路線,只是該路線因為站點較多,被程式過濾掉了。
2、對於相距太遠的兩個節點,查詢時間可能很長,甚至達到1分鐘之久。
為了解決上述問題,我對演算法進行了改進。
首先,將直達路線的優先順序設定為最高。就是說,如果兩個站點之間有直達路線,就不要選擇那些需要換乘的路線。
其次,經過抽樣統計,我發現直達或換乘一次就能到達目的地的概率高達80%,我相信地鐵建設人員在設計的時候就已經考慮過這個問題了。既然這樣,我可以對換乘一次的路線進行優先查詢。假設起點是A,終點是B,它們的換乘點是C,那麼,我可以先查出A->C的直達路線,再查出C->B的直達路線,然後將兩條路線合併即可,這樣可以顯著降低時間複雜度。
改進後代碼如下:
/// <summary> /// 查詢乘車路線。 /// </summary> /// <param name="startNode">起點。</param> /// <param name="endNode">終點。</param> /// <returns>乘車路線。</returns> public MetroPath FindPath(MetroNode startNode, MetroNode endNode) { MetroPath path = new MetroPath(); if (startNode == null || endNode == null) return path; if (startNode == endNode) return path; //如果起點和終點擁有共同線路,則查詢直達路線 path = FindDirectPath(startNode, endNode); if (path.Links.Count > 0) return path; //如果起點和終點擁有一個共同的換乘站點,則查詢一次換乘路線 path = FindOneTransferPath(startNode, endNode); if (path.Links.Count > 0) return path; //查詢路徑最短的乘車路線 var pathList = FindShortestPaths(startNode, endNode, null); //查詢換乘次數最少的一條路線 int minTransfers = int.MaxValue; foreach (var item in pathList) { var curTransfers = item.Transfers; if (curTransfers < minTransfers) { minTransfers = curTransfers; path = item; } } return path; } /// <summary> /// 查詢直達路線。 /// </summary> /// <param name="startNode">開始節點。</param> /// <param name="endNode">結束節點。</param> /// <returns>乘車路線。</returns> private MetroPath FindDirectPath(MetroNode startNode, MetroNode endNode) { MetroPath path = new MetroPath(); var startLines = startNode.Links.Select(c => c.Line).Distinct().ToList(); var endLines = endNode.Links.Select(c => c.Line).Distinct().ToList(); var lines = startLines.Where(c => endLines.Contains(c)).ToList(); if (lines.Count == 0) return path; //查詢直達路線 List<MetroPath> pathList = new List<MetroPath>(); foreach (var line in lines) { pathList.AddRange(FindShortestPaths(startNode, endNode, line)); } //挑選最短路線 return GetShortestPath(pathList); } /// <summary> /// 查詢一次中轉的路線。 /// </summary> /// <param name="startNode">開始節點。</param> /// <param name="endNode">結束節點。</param> /// <returns>乘車路線。</returns> private MetroPath FindOneTransferPath(MetroNode startNode, MetroNode endNode) { List<MetroPath> pathList = new List<MetroPath>(); foreach (var startLine in startNode.Links.Select(c => c.Line).Distinct()) { foreach (var endLine in endNode.Links.Select(c => c.Line).Where(c=> c != startLine).Distinct()) { //兩條線路的中轉站 foreach (var transferNode in this.Graph.GetTransferNodes(startLine, endLine)) { //起點到中轉站的直達路線 var startDirectPathList = FindShortestPaths(startNode, transferNode, startLine); //中轉站到終點的直達路線 var endDirectPathList = FindShortestPaths(transferNode, endNode, endLine); //合併兩條直達路線 foreach (var startDirectPath in startDirectPathList) { foreach (var endDirectPath in endDirectPathList) { var directPath = startDirectPath.Merge(endDirectPath); pathList.Add(directPath); } } } } } //挑選最短路線 return GetShortestPath(pathList); }
【總結】
MetroGraphApp程式主要是應用了圖論和GDI+的知識。最大的難點在於,如何更快地找出符合使用者需要的乘車路線。
傳統的深度遍歷方法不能很好地解決我們的問題,我們需要在遍歷之前,對前方的路徑進行一次偵測,然後把那些不需要的路徑全部“剪除”掉,這樣就可以顯著提高效能。
當然本文的演算法並不能保證任意兩點之間的路徑,都能夠在1秒之內找出,有些複雜的路徑搜尋還是會長達幾十秒,只是這樣的概率非常低。如果讀者感興趣,可以進一步研究改進。
原始碼下載(解壓縮密碼是:cnblogs)。
1、對於本文描述的演算法,以及提供的原始碼,保留所有權利。
2、本文的原始碼只是供大家學習、研究之用,不得用於商業目的。
3、如果想在其它網站或平臺轉載,請徵得本人同意。
4、介面上的“起點”和“終點”兩個圖示,來源於網路,版本歸原作者所有。
相關推薦
地鐵線路圖的設計與實現
在北京、上海這樣的一線城市,地鐵絕對是上班族的首選交通工具,儘管有時擠得要命,但你真的找不出比地鐵更準點的交通工具了。平時出門,我也總是習慣於在百度地圖或丁丁地圖裡先查詢一下地鐵乘車路線,這些程式用起來非常方便。最近幾天終於有點空餘時間了,我就在想,我是否也可以寫一個這樣的程式?作為一名專業碼農,我決定立
MVC之排球比賽計分程序 ——(四)視圖的設計與實現
元素 role view logs image 技術 size 之前 log (view)視圖 視圖是用戶看到並與之交互的界面。對老式的Web應用程序來說,視圖就是由HTML元素組成的界面,在新式的Web應用程序中,HTML依舊在視圖中扮演著重要的角色,但一些新的技術
中國象棋程式的設計與實現 一 專案截圖
上週一發表了,中國象棋程式的設計與實現(原始版)(包含原始碼) ,在一週的時間裡,有22次下載,700次訪問,還是挺讓我欣喜的。 本週和下週,將陸續發表中國象棋程式的設計與實現(高階版),包括 專案截圖,畢業論文,架構圖,開發日誌記錄,演算法設計等,更重要的是 專案的原始碼。簡
Python實現屬於自己的公交地鐵線路圖
本文主要講解的就是用Python計算公交線路圖的功能,即輸入起始點和結束點,即能夠得出公交的線路。 先說下資料的來源,直接網上爬取,也可以直接略過此點,直接下載我的原始碼獲取。 # coding=utf-8 import requests from bs4 import Beauti
jQuery架構設計與實現(2.1.4版本)
需要 引入 hasclass 8.4 uri and hub 組織 移除 市面上的jQuery書太多了,良莠不齊,看了那麽多總覺得少點什麽 對"幹貨",我不喜歡就事論事的寫代碼,我想把自己所學的知識點,代碼技巧,設計思想,代碼模式能很好的表達出來,所以考慮通過分析jQuer
畢業設計-證券宣傳手機微網站的設計與實現
信息 browser .com 接受 熱點 互聯網 計算機網絡 業務 結構 本文介紹基於.net的證券公司宣傳微網站手機網頁的設計與實現方法。 隨著計算機技術的快速發展,基於Web的計算機網絡金融、證券宣傳或交易網站已成為現代金融理財發展的熱點,B/S(Browser/Se
MVC實戰之排球計分(四)—— View設計與實現
service family 角色 元素 需要 rom 之前 con xsl (view)視圖 視圖是用戶看到並與之交互的界面。對老式的Web應用程序來說,視圖就是由HTML元素組成的界面,在新式的Web應用程序中,HTML依舊在視圖中扮演著重要的角色,但一些新的技術已層出
MVC實戰之排球計分(五)—— Controller的設計與實現
需要 strong 技術 ret web src alt 點擊 cnblogs 控制器 控制器接受用戶的輸入並調用模型和視圖去完成用戶的需求。所以當單擊Web頁面中的超鏈接和發送HTML表單時, 控制器本身不輸出任何東西和做任何處理。它只是接收請求並決定調用哪個模型構件去處
stm32視頻教程分享:心率檢測儀的設計與實現
stm32視頻教程分享:心率檢測儀的設計與實現 STM32系列是基於專為要求高性能、低成本、低功耗的嵌入式應用專門設計的ARM Cortex-M3內核。 本項目主要講述了通過心律傳感器采集我們的心律數據,然後通過串口傳送到上位機中,上位機用Qt
MVC之排球比賽計分程序 ——(三)model類的設計與實現
比賽 用戶 count class 包括 result 控制 類的設計 可能 實體類是現實實體在計算機中的表示。它貫穿於整個架構,負擔著在各層次及模塊間傳遞數據的職責。一般來說,實體類可以分為“貧血實體類”和“充血實體類”,前者僅僅保存實體的屬性,而後者還包含一些實體間的關
模型類的設計與實現(四)
介紹 傳遞數據 規則 添加 play using ota 實體類 重要 實體類是現實實體在計算機中的表示。它貫穿於整個架構,負擔著在各層次及模塊間傳遞數據的職責。 一般來說,實體類可以分為“貧血實體類”和“充血實體類”,前者僅僅保存實體的屬性,而後者還包含一些實體間的關系與
jQuery技術內幕:深入解析jQuery架構設計與實現原理
源碼 att root 功能 技術內幕 瀏覽器 sel 緩存 callbacks jQuery源碼(jquery-1.7.1.js)的總體結構:(function( window, undefined ) {// 構造jQuery對象 var jQuery = (fun
軟件設計與實現
一個 基礎 建模 分析 解決 是什麽 哪些 模型 動態 我們寫軟件就是要解決用戶的需求,我麽需要表達和傳遞下面的信息,在“需求分析”階段,我們要搞清楚在問題領域中的現實世界中,都有哪些實體,如何抽象出我們真正的關心的屬性,實體之間的關系是什麽,在這個基礎上,用戶的需求是什麽
自己主動升級系統的設計與實現(續2) -- 添加斷點續傳功能 (附最新源代碼)
blog down 決定 top lin dom itl com 關於 一.緣起 之前已經寫了兩篇關於自己主動升級系統OAUS的設計與實現的文章(第一篇、第二篇)。在為OAUS服務端添加自己主動檢測文件變更的功能(這樣每次部署版本號升級時,能夠節省非常多時間。
隊列順序存儲 - 設計與實現 - API函數
http 出隊 插入 tmp .cpp tdi tree 順序 位置 隊列是一種特殊的線性表 隊列僅在線性表的兩端進行操作 隊頭(Front):取出數據元素的一端 隊尾(Rear):插入數據元素的一端 隊列不同意在中間部位進行操作! queu
IM系統中聊天記錄模塊的設計與實現
人的 dex auto 由於 模型 速度 開發 構造 qlite 看到很多開發IM系統的朋友都想實現聊天記錄存儲和查詢這一不可或缺的功能,這裏我就把自己前段時間為傲瑞通(OrayTalk)開發聊天記錄模塊的經驗分享出來,供需要的朋友參考下。 一.總體設計 1.存儲位置
視頻教程免費分享:嵌入式stm32項目開發之心率檢測儀的設計與實現
視頻教程免費分享:嵌入式stm32項目開發之心率檢測儀的設計與實現 本課程主要基於心率檢測儀的設計與實現講解STM32開發技術,STM32開發板廣泛應用於儀器儀表、家用電器、醫用設備、航空航天、專用設備的智能化管理、機器人及過程控制等領域,完成數據監控、數據處理、數據傳遞等功
《Linux內核設計與實現》讀書筆記(八)- 中斷下半部的處理
sym dmesg 重新編譯 warn dad style lsp 之前 res 在前一章也提到過,之所以中斷會分成上下兩部分,是由於中斷對時限的要求非常高,需要盡快的響應硬件。 主要內容: 中斷下半部處理 實現中斷下半部的機制 總結中斷下半部的實現 中斷實現
《Linux內核設計與實現》讀書筆記(十二)- 內存管理
enable vmalloc 緩沖 turn lean png border 編譯 不一致 內核的內存使用不像用戶空間那樣隨意,內核的內存出現錯誤時也只有靠自己來解決(用戶空間的內存錯誤可以拋給內核來解決)。 所有內核的內存管理必須要簡潔而且高效。 主要內容: 內
《Linux內核設計與實現》讀書筆記(十六)- 頁高速緩存和頁回寫
第一次 源碼 進行 lose 減少 文件緩存 掩碼 recycle 創建 主要內容: 緩存簡介 頁高速緩存 頁回寫 1. 緩存簡介 在編程中,緩存是很常見也很有效的一種提高程序性能的機制。 linux內核也不例外,為了提高I/O性能,也引入了緩存機