博客地址
github repository:subwayGo
PSP表
Personal Software Process Stages | 模块文件或描述 | 实际花费时间(小时) |
---|---|---|
预计开发时间 | 5 | |
地铁系统文件解析 | model/subway_caches.py | 1 |
站点信息获取接口 | model/stations.py | 2 |
UI控制部分 | ui/mainUI.py | 1 |
UI显示部分 | ui/mainUI.py | 2 |
核心算法 | controller/subway_control.py | 1 |
代码复审及测试 | (自我测试,修改代码,提交修改) | 0.5 |
合计 | 7.5 |
软件界面及功能展示
初始界面
北京查找结果
csv文件内容(北京为例)
选取csv文件加载天津系统
切换 至天津并查找信息
功能
- 识别并载入特定格式的任意地铁系统文件
- 查看地铁线路信息
- 帮助用户推算出最优乘坐路线
模块
model/
import csv
import os
import pickle
from model.stations import Station
class SubwayCache:
def __init__(self, name):
# 地铁系统名字
name: str
self.name = name
'''
所有地铁线dict
键为线路名
值为线路下的站点列表
每个站点为一个元组(站点名,坐标,开通状态)
'''
self.lines: dict[str:list]
self.lines = {}
'''
所有站点
字典类型,key为站点名字,value为一个列表
列表嵌套列表表示同一站的不同线路
每条线路包含一个线路名,对应一个元组(坐标,开通状态)
'''
self.stations: dict[str:list]
self.stations = {}
# 所有站点
self.station_obj: Station
self.station_obj = []
self._generate_caches()
def get_station(self, name) -> Station: # 得到站点对象
for s in self.station_obj:
if s.name == name:
return s
return None
def get_line_stations(self, line) -> list: # 返回整条线路的站点信息
return self.lines[line]
def _generate_caches(self): # 读取文件并生成pk文件缓存
path = os.getcwd() + "/res/" + self.name
if not os.path.exists(path):
os.mkdir(path)
self._load_stations()
self._load_lines()
def _load_stations(self): # 读取站点信息缓存,未检测到缓存则产生
path = os.getcwd() + "/res/" + self.name + "/stations.pk"
if not os.path.exists(path):
self._generate_stations(path)
else:
with open(path, 'rb') as f:
self.stations = pickle.load(f)
self.station_obj.clear()
for (key, inf) in self.stations.items():
self.station_obj.append(Station(self.name, key, inf))
def _load_lines(self): # 读取线路信息缓存
path = os.getcwd() + "/res/" + self.name + "/lines.pk"
if not os.path.exists(path):
self._generate_lines(path)
else:
with open(path, 'rb') as f:
self.lines = pickle.load(f)
def _generate_stations(self, path): # 产生站点信息缓存
with open(os.getcwd() + "/res/" + self.name + ".csv", 'r', encoding="utf-8") as f:
reader = csv.reader(f)
f = 0
for station in reader:
f = 1
inf = [x.split(",") for x in station[1].split()]
self.stations[station[0]] = inf
if f == 0:
raise Exception
for (key, line) in self.stations.items():
for i, t in enumerate(line):
line[i] = [t[0], (int(t[1]), 1 if t[2] == "是" else 0)]
with open(path, 'wb+') as f:
pickle.dump(self.stations, f, )
def _generate_lines(self, path): # 产生线路信息缓存
all_lines = {}
for (key, line) in self.stations.items():
key: str
line: list
for i, t in enumerate(line):
t: list[str, tuple:int, int]
if t[0] not in all_lines:
all_lines[t[0]] = [(key, t[1][0], t[1][1])]
else:
all_lines[t[0]].append((key, t[1][0], t[1][1]))
for (line, station) in all_lines.items():
self.lines[line] = sorted(station, key=lambda x: x[1])
with open(path, 'wb+') as f:
pickle.dump(self.lines, f)
def delete_cache(self): # 删除缓存信息
os.remove(os.getcwd() + "/res/" + self.name + "/stations.pk")
os.remove(os.getcwd() + "/res/" + self.name + "/lines.pk")
此模块在程序启动时自动扫描缓存并读取,为算法提供数据接口
删除缓存对应于UI上的删除地铁系统操作
class Station: # 站点对象
def __init__(self, subway, key, inf):
self.subway = subway # 地铁系统名
self.name = key # 站点名
self.inf = {x[0]: x[1] for x in inf} # 这个站点所属的线路:(坐标,开通状态)
self.routes = None
self.min_line = ""
self.circle = {}
def __contains__(self, item): # 重载in关键字,方便判断该站点是否属于某条线路
for line in self.inf.keys():
if line == item:
return True
return False
def _is_circle(self, route) -> bool: # 判断所在路线是否是特殊情况--环路
k = route
if not self.routes:
with open(os.getcwd() + "/res/" + self.subway + "/lines.pk", 'rb') as f:
self.routes = pickle.load(f)
route = [x[0] for x in self.routes[route]]
for r in self.routes.keys():
if r == k:
continue
r2 = [x[0] for x in reversed(self.routes[r])]
for i in route:
if not (i in r2):
break
else:
self.circle[k] = r
self.circle[r] = k
return True
return False
def __sub__(self, other): # 重载标识符“-”,使站点对象能直接相减得出距离
value = 9999
self.min_line = ""
for line in self.inf.keys():
if self.inf[line][1] == 0 or line == self.min_line:
continue
if line in other and other.inf[line][1] == 1:
t = self.inf[line][0] - other.inf[line][0]
if self._is_circle(line):
if t < 0:
t = -t
if 2 * t > len(self.routes[line]):
t = len(self.routes[line]) - t
if t < value:
value = t
self.min_line = self.circle[line]
else:
if t < value:
value = t
self.min_line = line
else:
if 2 * t > len(self.routes[line]):
t = len(self.routes[line]) - t
if t < value:
value = t
self.min_line = line
else:
if t < value:
value = t
self.min_line = self.circle[line]
else:
t = t if t > 0 else -t
if t < value:
value, self.min_line = t, line
return value
def is_open(self) -> bool: # 检查是否开通
for (route, j) in self.inf.items():
if j[1] == 1:
return True
return False
def get_past_stations(self, other): # 得到两个换乘站之间的所有站点
self - other
route, s = self.min_line, []
route_inf = self.routes[self.min_line]
route_stations = [x[0] for x in route_inf]
if self.inf[route][0] < other.inf[route][0]:
s.extend(route_stations[self.inf[route][0]:other.inf[route][0]])
s.append(other.name)
else:
if self._is_circle(self.min_line):
s.extend(route_stations[self.inf[route][0]:])
s.extend(route_stations[:other.inf[route][0] + 1])
else:
s.extend(route_stations[other.inf[route][0]:self.inf[route][0]])
s.append(self.name)
s = list(reversed(s))
res = route + ": " + "->".join(s)
return res
"""
为选择起点终点时提供线路下的站点名列表
为查询路线详情提供站点坐标,站点名及其开通状态
"""
class Route:
def __init__(self, routes):
self.routes = routes
self.routes_name = list(routes)
def get_stations_name(self, index):
return [x[0] for x in self.routes.get(self.routes_name[index])]
def get_pos(self, index):
pos = [str(x[1] + 1) for x in self.routes.get(self.routes_name[index])]
pos[0] = "始"
pos[-1] = "终"
return pos
def get_stations_name_status(self, index):
return [(x[0], "是" if x[2] == 1 else "否") for x in self.routes.get(self.routes_name[index])]
controller/
import os
from model.route import Route
from model.subway_caches import SubwayCache
class SubwayControl:
def __init__(self):
self.current_subway_cache = None
self.subway_dirs = []
self.routes_start = None
self.routes_end = None
self.routes_list_start = []
self.routes_list_end = []
self.stations_start = None
self.stations_end = None
self.all_station = []
self.all_station_obj = {}
self.distance = {}
self.search_system()
def select_route_start(self, route_index): # 获得当前线路下所有站点 列表
self.stations_start = self.routes_start.get_stations_name(route_index)
def select_route_end(self, route_index):
self.stations_end = self.routes_end.get_stations_name(route_index)
def select_subway(self, subway_index):
self.current_subway_cache = SubwayCache(self.subway_dirs[subway_index])
self.routes_start = Route(self.current_subway_cache.lines)
self.routes_end = Route(self.current_subway_cache.lines)
self.all_station = self.current_subway_cache.stations.keys()
self.all_station_obj = {x.name: x for x in self.current_subway_cache.station_obj}
self.select_route_start(0)
self.select_route_end(0)
def search_system(self):
subways = []
path = os.getcwd() + "/res/"
for a, b, c in os.walk(path):
for i in b:
if os.path.exists(path + i + "/lines.pk") and os.path.exists(path + i + "/stations.pk"):
subways.append(i)
self.subway_dirs = subways
if len(self.subway_dirs) > 0:
self.select_subway(0)
def delete_pk(self):
self.current_subway_cache.delete_cache()
# 核心部分Dijskra算法
def _find_shortest_station(self) -> str: # 找到当前情况下最近且未被访问过的点
min_value = 9999
min_s = ""
for s in self.all_station:
if self.distance[s][1] == 0 and self.distance[s][0] < min_value:
min_s = s
min_value = self.distance[s][0]
return min_s
def best_path(self, start_station, end_station):
res = []
if not self.all_station_obj[start_station].is_open():
res.append("起点未开通")
if not self.all_station_obj[end_station].is_open():
res.append("终点未开通")
if len(res) > 0:
return res
start_obj = self.all_station_obj[start_station]
path = {x: start_station for x in self.all_station} # 记录路径
self.distance = {x: [start_obj - self.all_station_obj[x], 0] for x in self.all_station} # 记录离原点最短距离,以及是否被访问
self.distance[start_station][1] = 1
min_station = self._find_shortest_station()
# 循环终止条件,最近距离点都被访问过
while min_station != "":
for s in self.all_station:
sub = self.all_station_obj[min_station] - self.all_station_obj[s]
if sub + self.distance[min_station][0] < self.distance[s][0]:
self.distance[s][0] = sub + self.distance[min_station][0]
path[s] = min_station
self.distance[min_station][1] = 1
min_station = self._find_shortest_station()
# 格式化输出结果
line = [end_station]
while end_station != start_station:
end_station = path[end_station]
line.append(end_station)
line = list(reversed(line))
n = len(line) - 1
for i in range(n):
line[i] = self.all_station_obj[line[i]].get_past_stations(self.all_station_obj[line[i + 1]])
del line[n]
return line
UI/
界面设计及事件响应代码详见GitHub, 不再赘述
总结
编写项目最初的设计很重要,计划稍有遗漏可能会给已写的代码带来很大的改动,考虑过于随便则会给自己写代码的过程制造很多麻烦,此次项目锻炼了我开发过程中对时间的把控能力。