个人项目-地铁出行路线规划系统的设计与实现

博客地址

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

软件界面及功能展示

初始界面
个人项目-地铁出行路线规划系统的设计与实现_第1张图片
北京查找结果
个人项目-地铁出行路线规划系统的设计与实现_第2张图片
csv文件内容(北京为例)
个人项目-地铁出行路线规划系统的设计与实现_第3张图片
选取csv文件加载天津系统
个人项目-地铁出行路线规划系统的设计与实现_第4张图片
切换 至天津并查找信息
个人项目-地铁出行路线规划系统的设计与实现_第5张图片


功能

  • 识别并载入特定格式的任意地铁系统文件
  • 查看地铁线路信息
  • 帮助用户推算出最优乘坐路线

模块

  • 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, 不再赘述


总结

编写项目最初的设计很重要,计划稍有遗漏可能会给已写的代码带来很大的改动,考虑过于随便则会给自己写代码的过程制造很多麻烦,此次项目锻炼了我开发过程中对时间的把控能力。

你可能感兴趣的:(个人项目-地铁出行路线规划系统的设计与实现)