结对项目-地铁出行路线规划程序(续)

Posted HyperLeopard

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结对项目-地铁出行路线规划程序(续)相关的知识,希望对你有一定的参考价值。


欢迎到我的个人博客,获取更好的用户体验:www.beyondbin.com

欢迎大家关注我们的软工团队:http://www.cnblogs.com/Default1406/

鸣谢:感谢Lave Zhang启发了我们前端的设计思路,以及百度地图为我们提供的全国所有城市地铁的xml文件。

结对编程:邓楚云(HyperLeopard) 岳桐宇

结对编程

编程现场

 

 

结对编程评价

优点

  1. 相互监督,从而提升了工作效率,稳定了工作时长,有助于按时完成项目。
  2. 频繁交流,使得分析、设计、测试等更加全面和完善。
  3. 由于处在不断的相互代码审查中,有效的提高了代码质量。

缺点

  1. 无法保证结对双方工作时间一致,阻碍项目进展。
  2. 实际需要较强的表达和交流能力,存在一定的沟通问题。
  3. 在简单项目中,领航员的作用不大。

结对人员评价

邓楚云

优点

  1. 学习能力较强,可以快速掌握新技术。
  2. 性格风趣幽默,不拘小节。
  3. 对用户体验有较好的理解,懂得设计交互界面。

缺点

  1. 懒于在细小事情上进行沟通,会细微修改对方代码,造成一定的混乱。

岳桐宇

优点

  1. 自我要求高,追求完美。
  2. 坚持编码原则,保证了代码质量。
  3. 算法的理解和实现能力较强,适合后端编码。

缺点

  1. 生活作息不规范,不太好安排时间。

设计方法

Information Hiding(信息隐藏)

信息隐藏实际上就是封装机制。具备封装性的程序设计隐藏了某一方法的具体执行步骤,取而代之的是通过讯息传递机制传送讯息给它。封装是通过限制只有特定类别的物件可以存取这一特定类别的成员,而它们通常利用介面实作讯息的传入传出。所有的类与组件均通过接口进行访问,并且内部数据必须通过安全的访问函数以实现,可以充分切分软件结构,从而实现模块化。因此,举例来说,“狗”这个类有“吠叫()”的方法,这一方法定义了狗具体该通过什么方法吠叫。但是,其他人并不知道它到底是如何吠叫的。而当狗的吠叫被封装到类中,任何人都可以简单地使用。

Interface Design(接口设计)

程序设计的实践中,编程接口的设计首先要使软件系统的职责得到合理划分。良好的接口设计可以降低系统各部分的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合程度,从而提高系统的维护性和扩展性。应用程序接口是一组数量上千、极其复杂的函数和副程序,可让程序设计师做很多工作,譬如“读取文件”、“显示选单”、“在窗口中显示网页”等等。操作系统的API可用来分配内存或读取数据。许多系统应用程序借由API接口来实现,像是图形系统、资料库、Web服务,甚至是线上游戏。应用程序接口有诸多不同设计。用于快速执行的接口通常包括函数、常量、变量与数据结构。也有其它方式,如通过解释器,或是提供抽象层以遮蔽同API实现相关的信息,确保使用API的代码无需更改而适应实现变化。

Loose Coupling(松耦合)

耦合性是一种软件度量角度,是指一程序中,模块及模块之间通信或参数依赖的程度。松耦合性是结构良好程序的特性,松耦合性程序的可读性及可维护性会比较好。松耦合的目标是最小化依赖。松耦合这个概念主要用来处理可伸缩性、灵活性和容错这些需求。但是松耦合要付出使系统更加复杂的代价,松耦合意味着更多的开发以及维护工作量。一个例子:A系统作为服务提供方,与B1,B2,B3....Bx等服务消费方系统对接,使用紧耦合点对点的方式来系统集成,那么假如A系统如果更改了地址,那么B1,B2,B3...Bx系统都需要求相应的请求地址。说明系统和系统间严重依赖。要实现松耦合,通常的做法就是引入Mediator(中间层,也有翻译成中介者),在SOA中,这个中间层通常指的就是ESB(企业服务总线)。

契约式编程

优点

  1. 更优秀的设计。谨慎地运用契约式设计方法可以获得更优秀的设计,这是因为组件服务的提供方和使用方各自的义务被表述得更清晰,从而使设计更加系统化、更清楚、更简单。子类特性的重定义得到周密的控制。异常的运用系统化、一致化。
  2. 契约可以提高可靠性,因为编写契约可以帮助开发者更好地理解代码。契约有助于测试。理解更加清晰,因此代码更加可靠 如果你按照两种不同的方式表达同一件事情,就能更好地理解这件事。
  3. 更出色的文档。契约乃是类特性的公用视图中的固有成分,是值得信赖的文档。契约是精确的规范,同时也可以作为测试的可靠指导。

缺点

如果我们将每一个类都很详细地进行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();
    }
}

参考资料

  1. 地铁线路图的设计与实现
  2. 在WPF中自定义你的绘制
  3. Arcgis for js实现北京地铁的显示
  4. Windows Presentation Foundation

后端(岳桐宇负责)

因为本次程序要求能同时计算换乘最少和站点最少,因此我在原本的计算最短路径的spfa算法的基础上增添了路由表功能,每个能到达的节点都拥有一张路由表,表示到达当前节点的目前全部来自于不同线路的最优路径,因为对于换乘站点,只保存一条最优的路径是不够的。而在每次更新新节点的路由表的时候,需要遍历这个节点的前序节点所有的路由表,根据这张路由表更新新节点的路由表。最后从终点的路由表中选取一个站点数最少的路由。

以上是关于结对项目-地铁出行路线规划程序(续)的主要内容,如果未能解决你的问题,请参考以下文章

结对项目-地铁出行路线规划程序(续)

结对项目-地铁出行路线规划程序(续)

结对项目-地铁出行路线规划程序(续)

结对项目-地铁出行路线规划程序(续)

结对项目--地铁出行路线规划程序(续)

结对项目-地铁出行路线规划程序(续)