一、GitHub的网址
首先还是先附上GitHub 项目地址:https://github.com/lytning98/subway
队友的博客地址:传送门
二、PSP表格和预估时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | |
- Esitimate | 估计任务时间 | 30 | |
Development | 开发 | 1,095 | |
- Analysis | 需求分析 | 45 | |
- Design Spec | 生成设计文档 | 60 | |
- Design Review | 设计复审 | 20 | |
- Coding Standard | 代码规范 | 30 | |
- Design | 具体设计 | 180 | |
- Coding | 具体编码 | 600 | |
- Code Review | 代码复审 | 60 | |
- Test | 测试(自我测试、修改代码、提交修改) | 100 | |
Reporting | 报告 | 260 | |
- Test Report | 测试报告 | 50 | |
- Size Measurement | 计算工作量 | 10 | |
- Postmortem & Process Improvement Plan | 事后总结并提出过程改进计划 | 190 | |
合计 | 1,385 |
(由于是和队友协作完成,故表格时间为总开发时间)
二、小组分工
这回结对项目的队友是我的室友。由于对友参加过ACM比赛,比较擅长算法编写,编程能力比我强不少。为了保证二人的工作效率能保持在较高状态,尽快完成项目,经我们二人商议后,确定了各自的任务。
队友在这回的结对项目中主要负责如下方面:
- 最短路和遍历等图论算法编写、路径输出实现;
- 地铁地图信息的整合;
- 地铁地图信息与图论模型之间的转换;
因为C#比较好上手,加上自己也用C#写过游戏,因而我的任务相对轻松一些,主要负责如下方面:
- GUI 设计和 GUI 逻辑的实现;
- 地铁地图 JSON 信息的读取、保存;
- 实现对地铁地图信息的查询,整合各个模块功能;
三、概要设计
老师的项目主要要实现以下功能:
1.直接启动subway.exe会进入线路查询模式,用户输入要查询的地铁线路号后,程序就输出此线路的站名(按任意顺序);
2.输入subway.exe /b xxx xxxx(xxx和xxxx为两个不同站名),便可以得到两站间的最有效线路。在cmd中会输出站数、每站的站名和换乘信息;
3.输入subway.exe /a xxx(xxx为站名),便可以得到最快遍历所有地铁站的线路。在cmd中会输出站数和每站的站名;
4.输入subway.exe /g会进入GUI界面,在GUI界面中可以实现两站间做有效线路和最快遍历所有地铁站线路的可视化;
5.输入subway.exe /z xxx.txt(xxx.txt为文件名),xxx.txt放了形如subway.exe /a xxx中输出的答案(包括站数和站名),程序将会进行如下判断:
-若xxx.txt中的数据覆盖了所有地铁站至少一次,并且站数和遍历次序是对的,则输出true;
-若xxx.txt中遍历次序是对的,但存在站点遗漏的情况,则输出false;
-若xxx.txt中的遍历次序是不对的,则输出error。
根据上述要求,我们做了下述流程图。
根据分配的任务,我主要要完成是输入/g和/z以及线路查询部分。
四、部分代码说明
在本项目中,我一共编写了两个类(包括LineQuery和Map)和GUI界面。
LineQuery主要负责线路查询和答案验证(即/z功能),其中负责线路查询的函数模块如下:
public static void LineStations()
{
Console.WriteLine("输入地铁线路名(如“4号线”“房山线”,不含引号)查询该线路所有站点,输入“exit”退出");
Console.WriteLine("可选线路 : ");
foreach (SubwayLine sl in Map.subwayMap.Lines)
Console.WriteLine(" -> " + sl.Name);
while (true)
{
Console.Write(">>>> ");
string str = Console.ReadLine();
if (!Map.LineId.ContainsKey(str))
{
if (str == "exit")
return;
Console.WriteLine("线路名有误!");
}
else
{
int lid = Map.LineId[str];
foreach (string name in Map.subwayMap.Lines[lid].Path)
Console.WriteLine(" - " + name);
}
}
}
负责答案验证(即/z功能)的代码花费了我比较多的时间,也让我花了很长的时间Debug。开始因为将int current = Map.StationId[lines[0]]写成了int current = Map.StationId[lines[1]],同时i=0写成了i=1,导致假如第一站缺失时,程序仍然判定为true。具体代码如下:
public static void TestAnswer(string path)
{
List<string> lines = null;
try
{
lines = File.ReadLines(path).ToList();
}catch(Exception e)
{
Console.WriteLine("指定的文件 {0} 有误!", path);
}
lines.RemoveAt(0);
int test;
if (int.TryParse(lines[0], out test) == true)
lines.RemoveAt(0);
foreach(string line in lines)
if(!Map.StationId.ContainsKey(line))
{
Console.WriteLine("error");
Console.WriteLine("站点 {0} 有误或未收录", line);
return;
}
bool[] traveled = new bool[Map.StationCount];
int current = Map.StationId[lines[0]];
traveled[current] = true;
for(int i = 1; i < lines.Count; i++)
{
int next = Map.StationId[lines[i]];
traveled[next] = true;
if(!Map.RouteSet.Contains(new Tuple<int, int>(current, next)))
{
Console.WriteLine("error");
Console.WriteLine("路线有误:{0} -> {1}", lines[i - 1], lines[i]);
return;
}
current = next;
}
bool done = true;
List<string> stations = new List<string>();
for(int i = 0; i < traveled.Length; i++)
if(!traveled[i])
{
done = false;
stations.Add(Map.subwayMap.Stations[i].Name);
break;
}
if(!done)
{
Console.WriteLine("false");
Console.WriteLine("遗漏站点:");
foreach (string str in stations)
Console.WriteLine(str);
}else
{
Console.WriteLine("true");
}
return;
}
Map主要负责完成地铁图信息读取,是比较常规的一个类,比较重要的函数有两个。第一个是负责JSON解析的函数,具体代码如下:
private static bool LoadJson(string path)
{
try
{
string json = File.ReadAllText(path);
subwayMap = JsonConvert.DeserializeObject<Subway>(json);
}
catch (Exception e)
{
Console.WriteLine("地铁数据读取异常, 请确认数据文件 [{0}] 存在且完整!", path);
return false;
}
return true;
}
第二个是根据 JSON 信息计算各线路的地铁站和过站顺序等数据的函数,具体代码如下:
private static void CollectLines()
{
int lid = 0;
foreach (SubwayLine sl in subwayMap.Lines)
{
LineStations.Add(new List<int>());
LineId[sl.Name] = lid;
int lastStationId = -1;
foreach (string stationName in sl.Path)
{
int sid = StationId[stationName];
LineStations[lid].Add(sid);
if(!StationLines[sid].Contains(lid))
StationLines[sid].Add(lid);
if (lastStationId != -1)
{
RouteSet.Add(new Tuple<int, int>(lastStationId, sid));
RouteSet.Add(new Tuple<int, int>(sid, lastStationId));
}
lastStationId = sid;
}
lid++;
}
}
GUI中主要实现的功能是画路径,具体实现这个功能的有以下三个函数:
private void DrawDot(int id, Brush brush)
{
float x = Map.subwayMap.Stations[id].x, y = Map.subwayMap.Stations[id].y;
x *= ratioX; y *= ratioY;
G.FillEllipse(brush, x, y, 10, 10);
}
private void ShowPath()
{
MapBox.Refresh();
current = 0;
DrawDot(path[0].Item1, Brushes.Red);
timer1.Enabled = true;
}
private void timer1_Tick(object sender, EventArgs e)
{
DrawDot(path[current].Item1, Brushes.LightGreen);
current++;
if (current == path.Count)
{
timer1.Enabled = false;
InfoText.Text = string.Format("已完成,共{0}站", current);
return;
}
DrawDot(path[current].Item1, Brushes.Red);
InfoText.Text = string.Format("{0}站", current);
}
五、结果展示
控制台命令
1.在CMD中直接输入subway.exe可进入线路查询模式,程序将输出所有可供用户查询的线路名称。在用户输入要查询的线路名称后,程序将会输出此线路上所有地铁站的站名。输入exit后将退出线路查询模式。
2.输入subway.exe /b 起始站名称 终点站名称,即可查询从起始站到终点站最短路线。程序将输出途经的所有站名和必要的换乘信息。
3.输入subway.exe /a 起始站名称,即可查询从起始站出发最短遍历所有地铁站的路线。系统将按顺序输出途经所有地铁站的站名。
由于线路过长,站数过多,无法全部显示,因而我们加入了功能:输入subway.exe /b 起始站名称 > 文件名,可将输出信息保存在文件中。程序不再在控制台中输出信息。
4.输入subway.exe /z 文本文件路径,程序将依照判定规则判断文本文件格式为true/false/error,并给出必要的信息。
注意:测试的txt文档一定要是UTF-8编码,否则程序将会识别错误,一直输出error。
5.若输入格式不正确,系统将给出如下图所示的提示信息。
6.输入subway.exe /g,则将进入GUI界面。
GUI
GUI 对程序的求最短路径、求遍历路径两个功能提供了较为方便和直观的图形化交互方式。
寻路功能展示(GIF动图):
遍历功能展示(GIF动图,中途有剪辑):
如图所示,GUI 启动后会显示北京地铁地图和其他的操作控件,并在右上方显示当前已经经过的站点数量。用户选择一种功能,按要求输入站名并点击开始后,程序将根据计算出的路径开始在地图上动态地绘制路径,其中已经被经过的站点标记为绿色, 而当前所在的站点被标记为红色。
若输入站点错误,程序将会弹窗如下提示信息。
六、实际用时
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
- Esitimate | 估计任务时间 | 30 | 20 |
Development | 开发 | 1,095 | 1,295 |
- Analysis | 需求分析 | 45 | 30 |
- Design Spec | 生成设计文档 | 60 | 100 |
- Design Review | 设计复审 | 20 | 10 |
- Coding Standard | 代码规范 | 30 | 45 |
- Design | 具体设计 | 180 | 210 |
- Coding | 具体编码 | 600 | 800 |
- Code Review | 代码复审 | 60 | 40 |
- Test | 测试(自我测试、修改代码、提交修改) | 100 | 60 |
Reporting | 报告 | 260 | 425 |
- Test Report | 测试报告 | 50 | 40 |
- Size Measurement | 计算工作量 | 10 | 30 |
- Postmortem & Process Improvement Plan | 事后总结并提出过程改进计划 | 190 | 355 |
合计 | 1,385 | 1,740 |
七、收获与感想
实话说这次选第三个题目完全是队友的意愿,不过队友也承担下来了比较难的几个点,因而我的任务并不算重,但是就是这样我一如既往地写了不少bug,也因为对一些函数不够了解而绕了不少弯路。这里真的十分感谢我耐心&热心的队友,他在编码规范和重构代码以及Debug上给了我这个小白不少的指导。也教会我如何更好地封装类,让程序模块内部的内聚性更高,让模块间的耦合度更低,让编写的代码看起来更加清晰和简洁。此外他甚至还安利了我一些其他处理问题会用到的工具,帮助我省下了不少时间和精力。虽然是第一次和室友合作,但是个人的体验还算不错(可能队友的体验就没有这么好了,毕竟真的是帮我解决了不少难题,而且他一个人做的话可能进度会比两个人做还要快)。总的来说,相比在学期中写的个人项目,感觉自己的实力确实有了一定的提升,也希望自己以后能继续努力吧。