1. 程式人生 > >地鐵線路圖的設計與實現

地鐵線路圖的設計與實現

 在北京、上海這樣的一線城市,地鐵絕對是上班族的首選交通工具,儘管有時擠得要命,但你真的找不出比地鐵更準點的交通工具了。平時出門,我也總是習慣於在百度地圖或丁丁地圖裡先查詢一下地鐵乘車路線,這些程式用起來非常方便。最近幾天終於有點空餘時間了,我就在想,我是否也可以寫一個這樣的程式?作為一名專業碼農,我決定立刻動手。

    首先,我給地鐵線路圖程式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性能,也引入了緩存機