北京地鐵出行線路規划系統項目總結
GitHub倉庫地址:https://github.com/KeadinZhou/SE-Subway
Demo地址:http://10.66.2.161:8080/ (校內網)
項目需求
- 實現一個幫助進行地鐵出行路線規划的命令行程序
- 地鐵線路圖數據需要與執行程序解耦
- 支持查詢單條線路的所有站點
- 支持查詢任意兩站之間通過最少站數的路線
算法設計
項目中最主要的點在於:找出兩個站點之間通過最少站數的路線。該點對應的經典模型是“在一個無向圖中找出兩點之間的最短路徑”。各個站點即為無向圖中的點,兩站間的路徑即為無向圖中連接兩個節點的邊,在此需求下,所有邊的長度均為1
。
地鐵最短路的換乘,最貼近實際的情況是“一次初始化,多次查詢”,即在地圖確定下來之后可能會有若干次不同點對的查詢。而本需求中地鐵線路圖數據需要與執行程序解耦,每次查詢對於地圖數據都可能是不同的,所以初始化的時間應該均攤到每次查詢中去。由於地鐵地圖屬於稀疏圖,且該圖中每條邊權恆定,故可以直接使用BFS
算法解決。
細節上,為了解決兩條線路覆蓋的情況下(見下圖),連續換乘的問題,需要更好的換乘算法。
解決方法:通過判斷站點和前繼站點的所在線路是否有重疊來判斷是否需要換乘。
存儲設計
數據存儲上,可以采用存點/存邊兩種形式。在本項目中,存邊會明顯優於存點。
- 簡潔易懂。數據中每個條目表示兩個站點中的一條通路。
- 易於擴展。添加站點方便;數據不需要有序。
- 方便應用程度讀取。數據中每個條目即為圖中的一條邊,方便建圖。
具體到內容,整個數據文件可以被幾條線路分成幾個大的模塊,每個模塊存一條線路的數據,由於地鐵站名不存在同名的情況,可以不存線路換乘站點的信息,通過同名同名判斷即可。由於同一條邊不可能同時屬於兩條線路,所以在邊信息上不會存在冗余。每個模塊由一個星號(*
)開始,后面這個這條線的名稱。接下來包含若干條邊信息。每條邊信息的格式為起點站名稱 終點站名稱
。每條線的起點可以由建圖后入度為 0
的點確定。
樣例如下:
* 1號線
劉院 西橫堤
果酒場 本溪路
...(省略)
咸水沽北 雙橋河
* 9號線
天津站 大王庄
大王庄 十一經路
...(省略)
架構設計
系統整體的計算核心使用Java
實現,通過命令行可以與之進行數據交互,通過指定地圖數據和相關查詢指令,可以實現所有的需求。同時,該核心可以通過外加另外的展示模塊來實現人機交互的可視化。外加的數據顯示模塊采用B/S
架構,后端使用Python/Flask
實現,前端使用Node.js/Vue.js
實現,前后端分離,兩者使用接口進行數據交互。
實現細節
計算核心
數據初始化
private static void dataInit(String pathname) {
try(FileReader reader = new FileReader(pathname); BufferedReader br = new BufferedReader(reader) ) {
String line;
String lineName="";
boolean first=false;
while((line=br.readLine()) != null) {
String[] data = line.split(" ");
if(data[0].contains("*")){
lineName=data[1];
first=true;
continue;
}
int id1=getStationId(data[0]);
int id2=getStationId(data[1]);
if(first){
lineStart.put(lineName, id1);
first=false;
stations.get(id1).addLines(lineName);
}
stations.get(id2).addLines(lineName);
addEdge(id1, id2, lineName);
}
Station.count=stationCnt;
} catch (Exception e) {
e.printStackTrace();
}
}
通過約定的數據存儲格式進行數據初始化,通過 *
星號來區別不同線路的數據。每條數據表示地鐵圖上的一條邊,通過記錄邊數據和對應的線路名稱來建立地鐵圖。
換乘檢測
private static String checkSwitchLine(int id1, int id2, int id3){
String linea=edgeLines.get(getEdgeHash(id1,id2));
String lineb=edgeLines.get(getEdgeHash(id2,id3));
if(linea.equals(lineb)) return null;
return lineb;
}
通過檢測相鄰兩條表是否同屬於同一條線路來判斷是否需要換乘。
路徑查詢
private static void getShortestPath(String start, String end) throws IOException {
int startID=getStationId(start);
if(startID>Station.count){
println("錯誤:沒有 " + start + " 這個站");
return;
}
int endID=getStationId(end);
if(endID>Station.count){
println("錯誤:沒有 " + end + " 這個站");
return;
}
Queue<Integer>q=new LinkedList<>();
q.offer(startID);
stations.get(startID).setVis(true);
while(!q.isEmpty()){
Station now=stations.get(q.poll());
for(int it:now.getEdges()){
Station tmp=stations.get(it);
if(tmp.isVis()) continue;
q.offer(it);
tmp.setVis(true);
tmp.setLastPoint(now.getId());
if(it==endID) break;
}
}
Stack<Integer> path=new Stack<>();
int tip=endID;
while(tip!=startID){
path.push(tip);
tip=stations.get(tip).getLastPoint();
}
ArrayList<Integer> res=new ArrayList<>();
res.add(startID);
while(!path.empty()){
int now=path.pop();
res.add(now);
}
println(res.size());
int tmpa=-1,tmpb=-1;
for(int it:res){
if(tmpa!=-1){
String switchMsg=checkSwitchLine(tmpa,tmpb,it);
if(switchMsg!=null){
println(switchMsg);
}
}
println(stations.get(it).getName());
tmpa=tmpb;
tmpb=it;
}
}
通過BFS
算法進行最短路的尋路。找到路徑之后檢測路徑上的所有站點是否需要換乘。
后端接口
后端接口由Python/Flask
實現。Flask
是一個輕量級的web框架,在接口類后端應用上有很大的優勢。
全局數據接口
@app.route('/data', methods=['GET'])
def data():
lines = []
sites = set()
file = open('subway.txt', 'r')
for item in file:
ll = item.strip().split()
if ll[0].find('*') != -1:
lines.append(ll[1])
else:
sites.add(ll[0])
sites.add(ll[1])
return jsonify({
'lines': lines,
'sites': list(sites)
})
該接口用於查詢地鐵圖中出現的所有線路和所有站點的列表,通過直接讀取數據文件實現。
查詢接口
GET_ALL_CMD = 'java subway -map subway.txt -a %s -o out.txt'
GET_PATH_CMD = 'java subway -map subway.txt -b %s %s -o out.txt'
@app.route('/query', methods=['POST'])
def query():
json_data = request.get_json()
all = json_data['isAll']
if all:
os.system(GET_ALL_CMD % json_data['query'])
else:
os.system(GET_PATH_CMD % (json_data['query'][0], json_data['query'][1]))
file = open('out.txt', 'r')
res = []
for item in file:
res.append(item.strip())
return jsonify({
'res': res
})
該接口通過前端提交的數據,選擇查詢線路和查詢路徑對應的計算核心命令,來使用計算核心來獲取線路數據。
前端頁面
前端頁面是用戶和程序交互的非常重要的手段。本項目的前端頁面使用Vue.js
配合ElementUI
實現。
運行效果
計算核心
通過-a
命令顯示一條線上所有的站點。
通過-b
命令查詢兩站之間的最短路徑。
通過-o
命令指定結果輸出的文件。
計算核心的健壯性:不合法命令的檢測。
WEB展示前端
查詢主界面。(路徑查詢)
當查詢時,輸入關鍵字,系統將會自動匹配補全相關的站點供用戶選擇。
查詢的結果。顯示每一站的站名,並在需要換乘的站點后面標注換乘的線路。
線路站點查詢,可以查詢每條線路所包含的所有站點。
線路查詢結果。