结对项目-地铁出行路线规划程序(续)
欢迎到我的个人博客,获取更好的用户体验:www.beyondbin.com
欢迎大家关注我们的软工团队:http://www.cnblogs.com/Default1406/
鸣谢:感谢Lave Zhang启发了我们前端的设计思路,以及百度地图为我们提供的全国所有城市地铁的xml文件。
结对编程:邓楚云(HyperLeopard) 岳桐宇
结对编程
编程现场
结对编程评价
优点
- 相互监督,从而提升了工作效率,稳定了工作时长,有助于按时完成项目。
- 频繁交流,使得分析、设计、测试等更加全面和完善。
- 由于处在不断的相互代码审查中,有效的提高了代码质量。
缺点
- 无法保证结对双方工作时间一致,阻碍项目进展。
- 实际需要较强的表达和交流能力,存在一定的沟通问题。
- 在简单项目中,领航员的作用不大。
结对人员评价
邓楚云
优点
- 学习能力较强,可以快速掌握新技术。
- 性格风趣幽默,不拘小节。
- 对用户体验有较好的理解,懂得设计交互界面。
缺点
- 懒于在细小事情上进行沟通,会细微修改对方代码,造成一定的混乱。
岳桐宇
优点
- 自我要求高,追求完美。
- 坚持编码原则,保证了代码质量。
- 算法的理解和实现能力较强,适合后端编码。
缺点
- 生活作息不规范,不太好安排时间。
设计方法
Information Hiding(信息隐藏)
信息隐藏实际上就是封装机制。具备封装性的程序设计隐藏了某一方法的具体执行步骤,取而代之的是通过讯息传递机制传送讯息给它。封装是通过限制只有特定类别的物件可以存取这一特定类别的成员,而它们通常利用介面实作讯息的传入传出。所有的类与组件均通过接口进行访问,并且内部数据必须通过安全的访问函数以实现,可以充分切分软件结构,从而实现模块化。因此,举例来说,“狗”这个类有“吠叫()”的方法,这一方法定义了狗具体该通过什么方法吠叫。但是,其他人并不知道它到底是如何吠叫的。而当狗的吠叫被封装到类中,任何人都可以简单地使用。
Interface Design(接口设计)
程序设计的实践中,编程接口的设计首先要使软件系统的职责得到合理划分。良好的接口设计可以降低系统各部分的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合程度,从而提高系统的维护性和扩展性。应用程序接口是一组数量上千、极其复杂的函数和副程序,可让程序设计师做很多工作,譬如“读取文件”、“显示选单”、“在窗口中显示网页”等等。操作系统的API可用来分配内存或读取数据。许多系统应用程序借由API接口来实现,像是图形系统、资料库、Web服务,甚至是线上游戏。应用程序接口有诸多不同设计。用于快速执行的接口通常包括函数、常量、变量与数据结构。也有其它方式,如通过解释器,或是提供抽象层以遮蔽同API实现相关的信息,确保使用API的代码无需更改而适应实现变化。
Loose Coupling(松耦合)
耦合性是一种软件度量角度,是指一程序中,模块及模块之间通信或参数依赖的程度。松耦合性是结构良好程序的特性,松耦合性程序的可读性及可维护性会比较好。松耦合的目标是最小化依赖。松耦合这个概念主要用来处理可伸缩性、灵活性和容错这些需求。但是松耦合要付出使系统更加复杂的代价,松耦合意味着更多的开发以及维护工作量。一个例子:A系统作为服务提供方,与B1,B2,B3....Bx等服务消费方系统对接,使用紧耦合点对点的方式来系统集成,那么假如A系统如果更改了地址,那么B1,B2,B3...Bx系统都需要求相应的请求地址。说明系统和系统间严重依赖。要实现松耦合,通常的做法就是引入Mediator(中间层,也有翻译成中介者),在SOA中,这个中间层通常指的就是ESB(企业服务总线)。
契约式编程
优点
- 更优秀的设计。谨慎地运用契约式设计方法可以获得更优秀的设计,这是因为组件服务的提供方和使用方各自的义务被表述得更清晰,从而使设计更加系统化、更清楚、更简单。子类特性的重定义得到周密的控制。异常的运用系统化、一致化。
- 契约可以提高可靠性,因为编写契约可以帮助开发者更好地理解代码。契约有助于测试。理解更加清晰,因此代码更加可靠 如果你按照两种不同的方式表达同一件事情,就能更好地理解这件事。
- 更出色的文档。契约乃是类特性的公用视图中的固有成分,是值得信赖的文档。契约是精确的规范,同时也可以作为测试的可靠指导。
缺点
如果我们将每一个类都很详细地进行DbC,那也是一件很耗时、痛苦的没有必要的事情,正如你预防着小偷固然好,但是将除了自己之外的其他人都像防贼一样来防着也不合适一样。我们应该是适当地DbC。
作业体现
由于只是实验性地使用DbC进行设计。在对Core模块的路径规划功能的设计中应用了DbC,在保证调用操作前后应当属于何种状态,即前置条件,后置条件和不变式。前置条件发生在每个操作(方法,或者函数)的最开始,后置条件发生在每个操作的最后,不变式实际上是前置条件和后置条件的交集。违反这些操作会导致程序抛出异常。
单元测试
UML
软件设计
前端(邓楚云负责)
概述
最开始我的考虑和大多数同学一样,想将一个图片作为整个交互式地图的控件基底,对其进行控件制作。但是考虑到了图片这种数据形式拓展性极差,不符合软件的迭代发展方式,遂决定放弃这种粗犷的地图制作模式。由于缺少软件前端设计的基础,思路一度陷入了停滞阶段。很幸运的是我在网上查找资料时发现了两个既有价值的博文,一是Lave Zhang教会了我如何自定义绘制控件,二是从地铁网站截取地铁图XML文件,就此奠定了前端设计的基础。但是由于Lave Zhang的博文采用的WinForms框架,已经跟不上时代的潮流,我便操刀开始使用WPF改写他的全部设计方案,在这之中付出了大量的努力与尝试。然后结合作业要求添加了不少功能,和特性,整个软件有极强的拓展性。
设计分析
在左侧采用极大的版面作为地图显示框,也就是此次作业新增的核心功能——图形化交互,让用户对地图路线有直观的认识,并且可以直接通过点击地图进行相对操作。相对Lave Zhang的原始版本,增加了右侧文本控制栏,使得用户可以选择两种方式进行输入,得到相应结果。且两侧有相应的数据联动效果。这样了除了视觉效果外基本和当前的商业软件一致。整个页面分为4个区域,地图交互区
、城市选择框
、路径规划框
、路径信息框
,功能简洁明了,其介绍如下:
区域 | 功能 | 设计细节 |
---|---|---|
地图交互区 | 1. 绘制城市地铁路线图。 2. 响应用户对站点的选择。 3. 绘制相应的路线规划。 4. 显示经过的站点数。 | 1. 换乘站点采用实心同心圆突出标记。 2. 起点与终点均使用特定图标标记。 3. 动画效果模拟运行效果。 4. 显示路线规划时,遮罩路线外的地图区域。 |
城市选择框 | 1. 选择不同城市的地图。 | 1. 可以通过导入新城市的xml,并添加相应的索引,增添城市选项。 |
路径规划框 | 1. 选择路径规划方式。 2. 选择起始与终点站点。 | 1. 起始与终点站点在输入信息时,自动跳转到相应下拉栏条目。 |
路径信息框 | 1. 显示当前路径规划信息,包括“站点”与“地铁线”。 | 1. 与地图交互区路线一致。 |
代码解析
MainWindow
- MainWindow()构造函数
public MainWindow()
{
InitializeComponent();
this.subwayMap = BackgroundCore.GetBackgroundCore().SubwayMap;//获取Core地图信息
this.displayRouteUnitList = ((App)App.Current).DisplayRouteUnitList;//获取“公共”路径规划信息
this.listView_Route.ItemsSource = displayRouteUnitList;
this.comboBox_StartStation.ItemsSource = displayStationsName;//设置始发站点选择列表
this.comboBox_EndStation.ItemsSource = displayStationsName;//设置终点站点(与始发站点一致)选择列表
((App)App.Current).IsShortestPlaning = (bool)radioButton_Shortest.IsChecked;//设置“公共”路径方案
BackgroundCore.GetBackgroundCore().SelectFunction(this, ((App)App.Current).Args);//获取命令行信息,选择相应模式
}
- searchRoute()路径搜索函数
private void searchRoute()
{
……
if ((bool)radioButton_Shortest.IsChecked)//路径规划模式选择
mode = "-b";
else
mode = "-c";
Cursor = Cursors.Wait;
try
{
if (subwayMap.CurRoute != null)
subwayMap.CurRoute.Clear();
displayRouteUnitList.Clear();//清空当前路径规划信息
subwayMap.SetStartStation(comboBox_StartStation.Text);
subwayMap.SetEndStation(comboBox_EndStation.Text);
subwayMap.CurRoute = subwayMap.GetDirections(mode);//调用Core计算路径信息
if (subwayMap.CurRoute.Count == 0)
throw new Exception("起始/终点站点相同!");
displayRouteUnitList.Add(new DisplayRouteUnit(subwayMap.CurRoute[0].BeginStation.Name, subwayMap.CurRoute[0].LineName));//转化CurRoute为可显示的路径信息
foreach (Connection connection in (subwayMap.CurRoute))
{
displayRouteUnitList.Add(new DisplayRouteUnit(connection.EndStation.Name, connection.LineName));
}
this.subwayGraph.ResetFlashIndex();//重置路径动画闪烁站点指针
}
……
}
SubwayGraph
- OnTimedEvent()定时委托事件函数
private void OnTimedEvent(object sender, EventArgs e)//由于普通的定时器无法在WPF直接使用,需要通过Dispatcher添加到委托中进行使用
{
this.Dispatcher.Invoke(DispatcherPriority.Normal, new TimerDispatcherDelegate(refreshFlashStation));
}
- IntializeStationFlash()站点闪烁模块初始化
private void IntializeStationFlash()
{
Timer timer = new Timer(500);//初始化定时器,并设置500ms的执行间隔
timer.Elapsed += new ElapsedEventHandler(OnTimedEvent);//向定时器中添加站点闪烁委托事件
timer.AutoReset = true;//在第一次间隔就开始执行委托事件
timer.Enabled = true;
}
- refreshFlashStation()刷新闪烁站点函数
private void refreshFlashStation()
{
if (subwayMap != null && subwayMap.CurRoute != null && subwayMap.CurRoute.Count != 0)
{
if (flashStationIndex == subwayMap.CurRoute.Count)//当到达终点站重置闪烁站点指针,回到起始站点
{
prevFlashStationIndex = flashStationIndex;
flashStation = subwayMap.CurRoute[flashStationIndex - 1].EndStation;
flashStationIndex = 0;
}
else//递增闪烁站点指针
{
prevFlashStationIndex = flashStationIndex;
flashStation = subwayMap.CurRoute[flashStationIndex].BeginStation;
flashStationIndex++;
}
InvalidateVisual();//重绘控件
}
}
- OnRender()重载控件绘制函数
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
//绘制背景
drawBackground(dc);
//未初始化地图时不进行渲染
if (subwayMap == null)
return;
//滚动与缩放
dc.PushTransform(new TranslateTransform(scrollX, scrollY));
dc.PushTransform(new ScaleTransform(zoomScale, zoomScale));
//绘制地铁线路
drawSubwayGraph(dc);
//绘制当前乘车路线
drawCurRoute(dc);
//绘制闪烁点
drawFlashPoint(dc);
//绘制起点和终点
drawStartAndEndStations(dc);
//取消滚动与缩放
dc.Pop();
dc.Pop();
//绘制已经过站点数
drawPassedStationNum(dc);
//绘制遮挡框架
drawFrame(dc);
//绘制线路列表
drawLineList(dc);
}
- drawFrame()控件显示框架绘制函数
private void drawFrame(DrawingContext dc)
{
RectangleGeometry rect1 = new RectangleGeometry(new Rect(0, 0, this.ActualWidth, this.ActualHeight));//控件显示窗口
RectangleGeometry rect2 = new RectangleGeometry(new Rect(0, 0, ((App)App.Current).MainWindow.ActualWidth, ((App)App.Current).MainWindow.ActualHeight));//控件边框遮挡栏
GeometryGroup group = new GeometryGroup();//创建集合图形绘制组
group.Children.Add(rect1);
group.Children.Add(rect2);
group.FillRule = FillRule.EvenOdd;//设置交叉区域填充规则为透明填充
dc.DrawGeometry(Brushes.White, new Pen(Brushes.Black, 0), group);
}
- drawConnection()轨道段绘制函数
private void drawConnection(DrawingContext dc, Connection connection)
{
……
//单线轨道
if (connection.Type == 0)
dc.DrawLine(pen, pt1, pt2);
//双线并轨,Type = 1 或 2 为不同方向的平移(需要确保pt1与pt2一致)
else if (connection.Type > 0)
{
double scale = (pen.Thickness / 2) / Distance(pt1, pt2);
double angle = (double)(Math.PI / 2);
if (connection.Type == 2)//连接类型为2的情况下,设置angle为负数,使得线段向下平移
angle *= -1;
//平移线段
Point pt3 = Rotate(pt2, pt1, angle, scale);
Point pt4 = Rotate(pt1, pt2, -angle, scale);
dc.DrawLine(pen, pt3, pt4);
}
}
- drawStation()站点绘制函数
private void drawStation(DrawingContext dc, Station station)
{
int textYOffset = -12;
int textXOffset = 0;
//绘制地铁站圆圈
Pen pen = new Pen(new SolidColorBrush(Colors.Black), station.IsTransfer ? 1 : 0.5);
double r = station.IsTransfer ? 7 : 5;
dc.DrawEllipse(Brushes.White, pen, new Point(station.X, station.Y), r, r);
if (station.IsTransfer)//绘制更大的同心圆换乘车站
{
dc.DrawEllipse(Brushes.Black, pen, new Point(station.X, station.Y), r - 2, r - 2);
}
//绘制地铁站名
FormattedText formattedText = createFormattedText(station.Name, 9);
switch (station.Name)//处理特殊遮挡情况
{
case "清华东路西口":
case "马当路":
case "金融高新区":
textYOffset = -(int)(2 * formattedText.Height + 2 * r);
break;
case "森林公园南门":
textXOffset = -(int)(formattedText.Width + 5);
break;
}
dc.DrawText(formattedText, new Point(station.X + 3 + textXOffset, station.Y + formattedText.Height + r + textYOffset));
}
- drawCurRoute()当前规划路径绘制函数
private void drawCurRoute(DrawingContext dc)
{
if (subwayMap.CurRoute == null || subwayMap.CurRoute.Count == 0)
return;
//绘制白色遮罩层
Rect rc = new Rect(-scrollX / zoomScale, -scrollY / zoomScale, Math.Abs(((App)App.Current).MainWindow.ActualWidth / zoomScale), Math.Abs(((App)App.Current).MainWindow.ActualHeight / zoomScale));
dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(200, 245, 245, 245)), new Pen(Brushes.Black, 0), rc);
//绘制当前乘车路线
foreach (Connection connection in subwayMap.CurRoute)
{
//绘制路径
if (connection.Type >= 0)
{
drawConnection(dc, connection);
}
else
{
//如果是隐藏的路径,则取反向的可见路径
Connection visibleConnection = subwayMap.Connections.Find((Connection curConnection) => curConnection.Type >= 0 && curConnection.BeginStation.Name.Equals(connection.EndStation.Name) && curConnection.EndStation.Name.Equals(connection.BeginStation.Name) && curConnection.LineName.Equals(connection.LineName));
if (visibleConnection != null)
drawConnection(dc, visibleConnection);
}
//绘制站点
drawStation(dc, connection.BeginStation);
drawStation(dc, connection.EndStation);
}
}
- drawLineList()线路列表绘制函数
private void drawLineList(DrawingContext dc)
{
double maxNameLenth = 0;
//获取字符集字符最长长度
foreach (SubwayLine line in subwayMap.SubwayLines)
{
FormattedText formattedText = createFormattedText(line.Name, 12);
if (formattedText.Width > maxNameLenth)
maxNameLenth = formattedText.Width;
}
//遮罩层
Rect rc = new Rect(5, 5, 60 + maxNameLenth, (subwayMap.SubwayLines.Count + 1) * 15);
dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(180, 245, 245, 245)), new Pen(Brushes.Black, 0.5), rc);
//线路列表
double y = rc.Y + 15;
foreach (SubwayLine line in subwayMap.SubwayLines)
{
//绘制线路标示线
dc.DrawLine(new Pen(new SolidColorBrush(hexToColor(line.Color)), 5), new Point(rc.X + 10, y), new Point(rc.X + 50, y));
//绘制线路名
FormattedText formattedText = createFormattedText(line.Name, 12);
dc.DrawText(formattedText, new Point(rc.X + 55, y - formattedText.Height / 2));
y += 15;
}
}
- Rotate()坐标旋转函数
private Point Rotate(Point v, Point o, double angle, double scale)
{
//以o为源点,旋转v,角度为angle,缩放为scale
v.X -= o.X;
v.Y -= o.Y;
//坐标系点旋转公式,可以通过三角函数进行证明
double rx = scale * Math.Cos(angle);
double ry = scale * Math.Sin(angle);
double x = o.X + v.X * rx - v.Y * ry;
double y = o.Y + v.X * ry + v.Y * rx;
return new Point((int)x, (int)y);
}
- GetStationAt()指定坐标点站点获取函数
private Station GetStationAt(Point pt)
{
Point graphPt = ClientToGraph(pt);//将经过平移和缩放的点转化为原始点坐标
return subwayMap.Stations.FirstOrDefault((Station station) => stationRect(station).Contains(graphPt));//获取一定方形区域内对应坐标点的站点
}
- UserControl_MouseUp()鼠标释放事件处理函数
private void UserControl_MouseUp(object sender, MouseButtonEventArgs e)
{
Station station = GetStationAt(e.MouseDevice.GetPosition(this));//获取鼠标释放所处坐标的站点
if (station != null)//当获取站点非空时,进行起终站点设置
{
if (subwayMap.StartStation == null)//设置起始站点
{
subwayMap.SetStartStation(station.Name);
((MainWindow)((App)App.Current).MainWindow).comboBox_StartStation.Text = station.Name;
}
else//设置终点站点,并进行线路规划
{
……
}
}
else if (Distance(e.MouseDevice.GetPosition(this), mouseDownPoint) < 1)//是否发生拖拽
{
//确认未发生拖拽时(即为鼠标点击情况),清空当前所有路径规划信息
subwayMap.SetStartStation("");
subwayMap.SetEndStation("");
((MainWindow)((App)App.Current).MainWindow).comboBox_StartStation.Text = "";
((MainWindow)((App)App.Current).MainWindow).comboBox_EndStation.Text = "";
if (subwayMap.CurRoute != null)
subwayMap.CurRoute.Clear();
displayRouteUnitList.Clear();
flashStation = null;
}
InvalidateVisual();//重绘控件
}
- UserControl_MouseMove()鼠标移动时间处理函数
private void UserControl_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
//根据移动距离设置地图平移属性
scrollX += (e.GetPosition(this).X - mouseLastPoint.X);
scrollY += (e.GetPosition(this).Y - mouseLastPoint.Y);
//获取事件结束鼠标坐标
mouseLastPoint = e.GetPosition(this);
InvalidateVisual();
}
}
参考资料
后端(岳桐宇负责)
因为本次程序要求能同时计算换乘最少和站点最少,因此我在原本的计算最短路径的spfa算法的基础上增添了路由表功能,每个能到达的节点都拥有一张路由表,表示到达当前节点的目前全部来自于不同线路的最优路径,因为对于换乘站点,只保存一条最优的路径是不够的。而在每次更新新节点的路由表的时候,需要遍历这个节点的前序节点所有的路由表,根据这张路由表更新新节点的路由表。最后从终点的路由表中选取一个站点数最少的路由。