Click here to Skip to main content
Email Password   helpLost your password?

Contents

  • Introduction
  • What Does It Look Like, Then. Eh
  • What Does It do And How Does It Do It
  • Introduction

    I don't know how many of you know about my past, but I studied Artificial Intelligence and Computer Science (CSAI) for my degree. One of the assigments we had was to create a search over a cut down list of the London undergound stations. At the time I thought my textual search was ace, but I couldn't help but think it would be nice to have a go at that again using some nice UI code. Then low and behold WPF came along, and I got into it and started writing articles in it, and forgot all about my CSAI days.

    So time passes....zzzzzz

    I went on holiday, happy days, holiday end. bummer, back to work.

    So I thought I need something fun to get myself back into work mode, so I thought, I know I'll have a go at the London underground tube agent again, this time making it highly graphical using WPF of course.

    Now I know this is quite a niche article and it is not that re-usable to anyone out there, but I can assure you if you want to learn some WPF this is a good exemplar of what WPF can do, and how. So there is some merit in it.

    Anyways, this article as you can now probably imagine is all about a London underground search algorithm. It is actually a A* like algorithm, where the search is guided by a known cost and a heuristic, which is some custom guidance factor.

    There are some neat tricks in this article that I feel are good WPF practice and also some XLINQ/LINQ snippets which you may or may not have used before.

    So I hope there is enough interest in there for you, to carry on reading. I like it anyway.

    What Does It Look Like, Then. Eh

    Ah now that is the nice part, I have to say I for one am most pleased with how it looks. Here are a couple of screen shots. And he is the list of the highlights:

     

    When it first starts the lines are drawn as shown here

    When a solution path is found, it is also drawn on the diagram using a wider (hopefully) more visible brush, which you can see below. Also of note is that each station also has a nice tooltip which shows its own line connections

    When a solution path is found, a new information button will be shown in the bottom right of the UI which the user can use.

    This information button allows the user to see a textual description of the solution path

    When a solution path is found, it is also possible for the user to hide lines that are not part of the solution path which is achieved by using a context menu (right click) which will show the lines popup (as seen below), from where the user can toggle the visibility of any line that is allowed to be toggled (basically not part of the solution path). The lines that are part of the solution path will be disabled, so the user will not be able to toggle them.

    What Does It do And How Does It Do It

    In the next sections I will cover how all the individual parts work together to form a whole app.

    Loading Data

    So obviously one of the 1st things that needs to happen is that we need to load some data. The data is actually split into 2 parts

    In order to load this data these 2 files are embedded as resources and read out using different techniques. The reading of the initial data is inside a new worker item in the ThreadPool. Which allows the UI to remain responsive while the data is loading, even though there is not much the UI or user can do until the data is loaded. Still it's good practice.

    So how is the data loaded. Well we enqueue our worker like this, where this is happening in a specialized Canvas control called TubeStationsCanvas which is located inside another called DiagramViewer which in turn is inside the MainWindow who has a ViewModel called PlannerViewModel set as it's DataContext. The PlannerViewModel is the central backbone object of the application. The PlannerViewModel  stores a Dictionary<String,StationViewModel>. As such most other controls within the app, will need to interact with the PlannerViewModel at some stage, this is sometimes achieved using direct XAML Binding, or in some other cases is realized as shown below, where we get the current Window and get its DataContext which is cast to a PlannerViewModel.

    
    if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
        return;
    
    vm = (PlannerViewModel)Window.GetWindow(this).DataContext;
    
    ThreadPool.QueueUserWorkItem((stateObject) =>
    {
        #region Read in stations
        ......
        ......
    
        #endregion
    
        #region Setup start stations
        ......
        ......
        #endregion
    
        #region Setup connections
        ......
        ......
        #endregion
    
        #region Update UI
        ......
        ......
        #endregion
    
    
    }, vm);
    

    Where the state object being passed to the worker is an instance of the main PlannerViewModel that is being used by the MainWindow DataContext. The PlannerViewModelis the top level ViewModel that coordinates all user actions.

    Anyway getting back to how the data is loaded, the stations themselves are loaded by reading the embedded resource stream for the "StationLocations.csv" file. It can also be seen below that the read in coordinates are scaled against the TubeStationsCanvas Width and Height to allow the stations to be positioned correctly. Basically what happens is that for each station read in from the CSV file a new StationViewModel is added to the Dictionary<String,StationViewModel> in the PlannerViewModel. Then a new StationControl is created and has its DataContext set to the StationViewModel which was added to the Dictionary<String,StationViewModel> in the PlannerViewModel. And then the StationControlis positioned within the TubeStationsCanvas using some DPs.

    vm.IsBusy = true;
    vm.IsBusyText = "Loading Stations";
    
    Assembly assembly = Assembly.GetExecutingAssembly();
    using (
        TextReader textReader =
            new StreamReader(assembly.GetManifestResourceStream
    		("TubePlanner.Data.StationLocations.csv")))
    {
        String line = textReader.ReadLine();
        while (line != null)
        {
            String[] parts = line.Split(',');
            vm.Stations.Add(parts[0].Trim(), 
    		new StationViewModel(parts[0].Trim(), 
    		parts[1].Trim(),
    	        Int32.Parse(parts[2]), 
    		Int32.Parse(parts[3]), 
    		parts[4].Trim()));
            line = textReader.ReadLine();
    
        }
    }
    
    Decimal factorX = maxX - minX;
    Decimal factorY = maxY - minY;
    
    this.Dispatcher.InvokeIfRequired(() =>
    {
        Double left;
        Double bottom;
        foreach (var station in vm.Stations.Values)
        {
            StationControl sc = new StationControl();
            left = this.Width * 
                (station.XPos - minX) / (maxX - minX); 
    
            bottom = this.Height * 
                (station.YPos - minY) / (maxY - minY);
    
            left = left > (this.Width - sc.Width)
                                      ? left - (sc.Width)
                                      : left;
            sc.SetValue(Canvas.LeftProperty, left);
    
            bottom = bottom > (this.Height - sc.Height)
                                      ? bottom - (sc.Height)
                                      : bottom;
            sc.SetValue(Canvas.BottomProperty, bottom);
    
            station.CentrePointOnDiagramPosition =
                new Point(left + sc.Width / 2,
                          (this.Height - bottom) - (sc.Height / 2));
            //set DataContext
            sc.DataContext = station;
    
            //add it to Canvas
            this.Children.Add(sc);
        }
    });
    

    And the station connections are read in as follows using some XLINQ over the embedded resource stream for the "StationConnections.xml" file. What happens is that the station read in from the XML file is retrieved from the Dictionary<String,StationViewModel> in the PlannerViewModel, and then the retreived StationViewModel has its connections added to for the currently read line from the XML file. This is repeated for all read in lines and the stations on the read in lines.

    XElement xmlRoot = XElement.Load("Data/StationConnections.xml");
    IEnumerable<XElement> lines = xmlRoot.Descendants("Line");
    
    //For each Line get the connections
    foreach (var line in lines)
    {
        Line isOnLine = (Line)Enum.Parse(typeof(Line), 
            line.Attribute("Name").Value);
    
        //now fetch the Connections for all the Stations on the Line
        foreach (var lineStation in line.Elements())
        {
            //Fetch station based on it's name
            StationViewModel currentStation =
                vm.Stations[lineStation.Attribute("Name").Value];
    
            //Setup Line for station
            if (!currentStation.Lines.Contains(isOnLine))
                currentStation.Lines.Add(isOnLine);
    
            //Setup Connects to, by fetching the Connecting Station
            //and adding it to the currentStations connections
            StationViewModel connectingStation =
                vm.Stations[lineStation.Attribute("ConnectsTo").Value];
    
            if (!connectingStation.Lines.Contains(isOnLine))
                connectingStation.Lines.Add(isOnLine);
    
            currentStation.Connections.Add(
                new Connection(connectingStation, isOnLine));
        }
    }
    

    Drawing The Lines

    As I just mentioned there is a specialized Canvas control called TubeStationsCanvas whos job it is to render the station connections and the searches solution path. We just covered how the stations are actually loaded in the first place, so how do the connection get drawn. Well the trick is to know about what the start Stations are. This is also done in the TubeStationsCanvas prior to attempting to render any connections. If we consider one line as an example, say Jubilee, and look at how that works.

    First we set up a start Station for the line in the TubeStationsCanvas as follows:

    List<StationViewModel> jublieeStarts = new List<StationViewModel>();
    jublieeStarts.Add(vm.Stations["Stanmore"]);
    startStationDict.Add(Line.Jubilee, jublieeStarts);
    

    By doing this it allows us to grab the start of a line, and from there grab all the connections using a bit of LINQ.

    Here is an example for the Jubilee line, again this is all done in the TubeStationsCanvas. In this case it is using an override of the void OnRender(DrawingContext dc), which exposes a DrawingContext parameter (think OnPaint() in winforms) which allows us to well er, Draw stuff.

    protected override void OnRender(DrawingContext dc)
    {
        base.OnRender(dc);
    
        try
        {
            if (isDataReadyToBeRendered && vm != null)
            {
     	    .....
     	    .....
     	    .....
    
                //Jubilee line
                if (vm.JubileeRequestedVisibility)
                {
                    var jublieeStartStations = startStationDict[Line.Jubilee];
                    DrawLines(dc, Line.Jubilee, Brushes.Silver, jublieeStartStations);
                }
    	    
     	    .....
     	    .....
     	    .....
    
            }
        }
        catch (Exception ex)
        {
            messager.ShowError("There was a problem setting up the Stations / Lines");
        }
    }

    It can be seen that this makes use of a method called DrawLines(), so let's continue our Drawing journey.

    private void DrawLines(DrawingContext dc, Line theLine,
        SolidColorBrush brush, IEnumerable<StationViewModel> stations)
    {
        foreach (var connectedStation in stations)
        {
            DrawConnectionsForLine(theLine, dc, brush, connectedStation);
        }
    }
    

    And this in turn calls the DrawConnectionsForLine() method, which is shown below

    private void DrawConnectionsForLine(Line theLine, DrawingContext dc,
        SolidColorBrush brush, StationViewModel startStation)
    {
        Pen pen = new Pen(brush, 25);
        pen.EndLineCap = PenLineCap.Round;
        pen.DashCap = PenLineCap.Round;
        pen.LineJoin = PenLineJoin.Round;
        pen.StartLineCap = PenLineCap.Round;
        pen.MiterLimit = 8;
    
        var connections = (from x in startStation.Connections
                           where x.Line == theLine
                           select x);
    
        foreach (var connection in connections)
        {
            dc.DrawLine(pen,
                startStation.CentrePointOnDiagramPosition,
                connection.Station.CentrePointOnDiagramPosition);
        }
    
        foreach (var connection in connections)
            DrawConnectionsForLine(theLine, dc, brush, connection.Station);
    
    }
    

    It can be seen that the DrawConnectionsForLine() method, is called recursively, ensuring ALL connections are drawn.

    That is how the connection drawing is done for all lines.

    Panning The Diagram

    As I stated at the start of the article the diagramming component supports panning. This is realized much easier than one would think. Its bascally all down to a specialized ScrollViewer called PanningScrollViewer, which contains the TubeStationsCanvas. The entire code for the PanningScrollViewer is as shown below.

    /// <summary>
    /// A panning scrollviewer
    /// </summary>
    public class PanningScrollViewer : ScrollViewer
    {
        #region Data
        // Used when manually scrolling
        private Point scrollStartPoint;
        private Point scrollStartOffset;
    
        #endregion
    
        #region Mouse Events
        protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
        {
            if (IsMouseOver)
            {
                // Save starting point, used later when determining how much to scroll.
                scrollStartPoint = e.GetPosition(this);
                scrollStartOffset.X = HorizontalOffset;
                scrollStartOffset.Y = VerticalOffset;
    
                // Update the cursor if can scroll or not.
                this.Cursor = (ExtentWidth > ViewportWidth) ||
                    (ExtentHeight > ViewportHeight) ?
                    Cursors.ScrollAll : Cursors.Arrow;
    
                this.CaptureMouse();
            }
    
            base.OnPreviewMouseDown(e);
        }
    
    
        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            if (this.IsMouseCaptured)
            {
                // Get the new scroll position.
                Point point = e.GetPosition(this);
    
                // Determine the new amount to scroll.
                Point delta = new Point(
                    (point.X > this.scrollStartPoint.X) ?
                        -(point.X - this.scrollStartPoint.X) :
                        (this.scrollStartPoint.X - point.X),
    
                    (point.Y > this.scrollStartPoint.Y) ?
                        -(point.Y - this.scrollStartPoint.Y) :
                        (this.scrollStartPoint.Y - point.Y));
    
                // Scroll to the new position.
                ScrollToHorizontalOffset(this.scrollStartOffset.X + delta.X);
                ScrollToVerticalOffset(this.scrollStartOffset.Y + delta.Y);
            }
    
            base.OnPreviewMouseMove(e);
        }
    
    
    
        protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
        {
            if (this.IsMouseCaptured)
            {
                this.Cursor = Cursors.Arrow;
                this.ReleaseMouseCapture();
            }
    
            base.OnPreviewMouseUp(e);
        }
        #endregion
    }
    

    Where there is also a small XAML Style required which is as follows:

    <!-- scroll viewer -->
    <Style x:Key="ScrollViewerStyle"
           TargetType="{x:Type ScrollViewer}">
        <Setter Property="HorizontalScrollBarVisibility"
                Value="Hidden" />
        <Setter Property="VerticalScrollBarVisibility"
                Value="Hidden" />
    </Style>
    

    And here is the PanningScrollViewer hosting the TubeStationsCanvas

    <local:PanningScrollViewer x:Name="svDiagram" Visibility="{Binding Path=IsBusy, 
        Converter={StaticResource BoolToVisibilityConv}, ConverterParameter=False}"
                             Style="{StaticResource ScrollViewerStyle}">
        <local:TubeStationsCanvas x:Name="canv" Margin="50"
                Background="Transparent"
                Width="7680"
                Height="6144">
        </local:TubeStationsCanvas>
    </local:PanningScrollViewer>
    

    Zooming The Diagram

    Zooming, well credit where credit is due, is more than likely sourced for all zooming WPF apps from Vertigos sublime initial WPF exemplar, the The Family Show, which was and still is one of the best uses of WPF I have seen.

    Anyway to cut a long story short the zooming is realized using a standard WPF Slidercontrol (albeit I have jazzed it up a bit with a XAML Style) and a ScaleTransform.

    public Double Zoom
    {
        get { return ZoomSlider.Value; }
        set
        {
            if (value >= ZoomSlider.Minimum && value <= ZoomSlider.Maximum)
            {
                canv.LayoutTransform = new ScaleTransform(value, value);
                canv.InvalidateVisual();
    
                ZoomSlider.Value = value;
                UpdateScrollSize();
                this.InvalidateVisual();
            }
        }
    }
    

    The user can zoom using the Slider as shown in the app :

    Station Line Tooltips

    I thought it may be nice to have each StationControl render a tooltip which ONLY shows the lines that the StationControl is on. This is done using standard Binding, where the StationControls ToolTip binds to the StationViewModel which is the DataContext for the StationControl. The StationViewModel contains a IList<Line> which represents the lines the station is on. So from there it is just a question of making the ToolTip look cool. Luckily this is what WPF is good at. Here is how.

    First the StationControl.ToolTip which is defined as shown below

    <UserControl.ToolTip>
    
        <Border HorizontalAlignment="Stretch"
                            SnapsToDevicePixels="True"
                            BorderBrush="Black"
                            Background="White"
                            VerticalAlignment="Stretch"
                            Height="Auto"
                            BorderThickness="3"
                            CornerRadius="5">
            <Grid Margin="0" Width="300">
                <Grid.RowDefinitions>
                    <RowDefinition Height="40"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                
                <Border CornerRadius="2,2,0,0" Grid.Row="0" 
                        BorderBrush="Black" BorderThickness="2"
                        Background="Black">
                    <StackPanel Orientation="Horizontal" >
                        <Image Source="../Images/tubeIconNormal.png"
                                   Width="30"
                                   Height="30" 
                                   VerticalAlignment="Center" 
                                   HorizontalAlignment="Left" 
                                   Margin="2"/>
                        <Label Content="{Binding DisplayName}"
                                   FontSize="14"
                                   FontWeight="Bold"
                                   Foreground="White"
                                   VerticalContentAlignment="Center"
                                   Margin="5,0,0,0" />
    
                    </StackPanel>
    
                </Border>
    
                <StackPanel Orientation="Vertical" Grid.Row="1">
    
                <!-- Bakerloo -->
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch"
                            Visibility="{Binding RelativeSource={RelativeSource Self}, 
                            Path=DataContext, Converter={StaticResource OnLineToVisibilityConv}, 
                                ConverterParameter='Bakerloo'}">
    
                    
                   <Border BorderBrush="Black" BorderThickness="2" 
                           CornerRadius="5"
                           Background="Brown" Height="30" Width="30" 
                           Margin="10,2,10,2">
                        <Image Source="../Images/tubeIcon.png" Width="20" 
                               Height="20" HorizontalAlignment="Center"
                               VerticalAlignment="Center"/>
                    </Border>
    
                    <Label Content="Bakerloo" 
                           Padding="2"
                           Foreground="Black"
                           FontSize="10"
                           FontWeight="Bold"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center" 
                           VerticalContentAlignment="Center"
                           HorizontalContentAlignment="Center"/>
    
    
                </StackPanel>
    
    	    <!-- Other lines done the same as Bakerloo -->
    	    <!-- Other lines done the same as Bakerloo -->
    	    <!-- Other lines done the same as Bakerloo -->
    	    <!-- Other lines done the same as Bakerloo -->
    	    <!-- Other lines done the same as Bakerloo -->
    	    <!-- Other lines done the same as Bakerloo -->
    
    
                <Label Content="{Binding ZoneInfo}"
                    FontSize="10"
                    Foreground="Black"
                    FontWeight="Bold"
                    VerticalContentAlignment="Center"
                    Margin="5" />
                    
    
                </StackPanel>
    
            </Grid>
    
    
        </Border>
    
    </UserControl.ToolTip>
    

    And there is a Style in the /Resources/AppStyles.xaml ResourceDictionary which dictates what ToolTips should look like. Here is that XAML

    <!-- Tooltip-->
    <Style TargetType="{x:Type ToolTip}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ToolTip}">
                    <Border BorderBrush="Black" CornerRadius="3" 
                            Margin="10,2,10,2"
                            Background="White" BorderThickness="1"
                            HorizontalAlignment="Center" 
                            VerticalAlignment="Center">
    
                        <Label Content="{TemplateBinding Content}" 
                           Padding="2"
                           Foreground="Black"
                           FontSize="10"
                           FontWeight="Bold"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center" 
                           VerticalContentAlignment="Center"
                           HorizontalContentAlignment="Center"/>
    
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    This looks quite cool when you hover over a StationControl, it will look like the following, which is quite useful when you are mousing over various stations on the diagram. Oh also the actual StationControl grows and shrinks when you mouse over it, this also aids in showing the user which station the ToolTip is for.

    The Guided Search

    As I previously mentioned the search is a A* type of search where we have a known cost and a unknown cost, Wikipedia has this to say about A* algorithm.

    In computer science, A* (pronounced "A star") is a best-first graph search algorithm that finds the least-cost path from a given initial node to one goal node(out of one or more possible goals).

    It uses a distance-plus-cost heuristic function (usually denoted f(x)) to determine the order in which the search visits nodes in the tree. The distance-plus-cost heuristic is a sum of two functions:

    The h(x) part of the f(x) function must be an admissible heuristic; that is, it must not overestimate the distance to the goal. Besides, h(x) must be such that f(x) cannot decrease when a new step is taken. Thus for an application like routing, h(x) might represent the straight-line distance to the goal, since that is physically the smallest possible distance between any two points (or nodes for that matter).

    http://en.wikipedia.org/wiki/A*_search_algorithm

     

    The search algorithm as used in the app, is described by the figure shown below. The search is initiated from a ICommand in the PlannerViewModel which is triggered from the user clicking the button on the UI.

    Where the actual code looks like this:

    public SearchPath DoSearch()
    {
    
        pathsSolutionsFound = new List<SearchPath>();
        pathsAgenda = new List<SearchPath>();
    
        SearchPath pathStart = new SearchPath();
        pathStart.AddStation(vm.CurrentStartStation);
        pathsAgenda.Add(pathStart);
    
        while (pathsAgenda.Count() > 0)
        {
            SearchPath currPath = pathsAgenda[0];
            pathsAgenda.RemoveAt(0);
            if (currPath.StationsOnPath.Count(
                x => x.Name.Equals(vm.CurrentEndStation.Name)) > 0)
            {
                pathsSolutionsFound.Add(currPath);
                break;
            }
            else
            {
                StationViewModel currStation = currPath.StationsOnPath.Last();
                List<StationViewModel> successorStations = 
                    GetSuccessorsForStation(currStation);
    
                foreach (StationViewModel successorStation in successorStations)
                {
                    if (!currPath.StationsOnPath.Contains(successorStation) &&
                        !(ExistsInSearchPath(pathsSolutionsFound, successorStation)))
                    {
                        SearchPath newPath = new SearchPath();
                        foreach (StationViewModel station in currPath.StationsOnPath)
                            newPath.StationsOnPath.Add(station);
    
                        newPath.AddStation(successorStation);
                        pathsAgenda.Add(newPath);
                        pathsAgenda.Sort();
                    }
                }
            }
        }
    
    
        //Finally, get the best Path, this should be the 1st one found due
        //to the heuristic evaluation performed by the search
        if (pathsSolutionsFound.Count() > 0)
        {
            return pathsSolutionsFound[0];
        }
        return null;
    
    }

    It can be seen that the Search returns a SearchPath object. It is in the SearchPath objects that the costs are stored. There are several costs associated with the search algorithm.

    HCost

    An admissible "heuristic estimate" of the distance to the goal (usually denoted h(x)).

    In terms of the attached application, this is simply the difference between two objects, which is coded as follows:

    public static Double GetHCost(StationViewModel currentStation, StationViewModel endStation)
    {
        return Math.Abs(
            Math.Abs(currentStation.CentrePointOnDiagramPosition.X - 
                endStation.CentrePointOnDiagramPosition.X) +
            Math.Abs(currentStation.CentrePointOnDiagramPosition.Y - 
                endStation.CentrePointOnDiagramPosition.Y));
    }
    

    GCost

    The path-cost function, which is the cost from the starting node to the current node (usually denoted g(x))

    In terms of the attached application, this is calculated using the number of line changes to get to the current station * SOME_CONSTANT

    public Double GCost
    {
        get { return gCost + (changesSoFar * LINE_CHANGE_PENALTY); }
    }
    

    FCost

    This is the magic figure that guides the search, when the SearchPaths in the Agenda are sorted. The FCost is nothing more than GCost + FCost. So by sorting the Agenda SearchPaths on this value, the supposed best route is picked.

    Drawing The Solution Path

    Drawing the solution path is simliar to what we saw to draw the actual lines, except this time, we are only looping through the StationViewModels of the SolutionFound SearchPath object and drawing the connections in the StationViewModels in the solution path.

    private void DrawSolutionPath(DrawingContext dc, SearchPath solutionPath)
    {
        for (int i = 0; i < solutionPath.StationsOnPath.Count() - 1; i++)
        {
            StationViewModel station1 = solutionPath.StationsOnPath[i];
            StationViewModel station2 = solutionPath.StationsOnPath[i + 1];
            SolidColorBrush brush = new SolidColorBrush(Colors.Orange);
            brush.Opacity = 0.6;
    
            Pen pen = new Pen(brush, 70);
            pen.EndLineCap = PenLineCap.Round;
            pen.DashCap = PenLineCap.Round;
            pen.LineJoin = PenLineJoin.Round;
            pen.StartLineCap = PenLineCap.Round;
            pen.MiterLimit = 10.0;
            dc.DrawLine(pen,
                station1.CentrePointOnDiagramPosition,
                station2.CentrePointOnDiagramPosition);
        }
    }
    

    This then looks like this on the diagram, note the orange path drawn, that is the Solution path.

    Hiding Unwanted Lines

    Whilst it is nice to see all the lines in the diagram, there is a lot to display and it would be nice if we could hide unwanted lines that are not part of the SolutionFound SearchPath. To this end, the attached demo code hosts a Popup window with a CheckBox per line, where the CheckBox for a given line is only Enabled if the SolutionFound SearchPath does not contain the line the checkbox is for.

    Assuming that the SolutionFound SearchPath does not contain a given line the user is able to toggle the lines Visibility using the CheckBox .

    Here is a screen shot of the line Visibility Popup and the affect of unchecking one of the CheckBoxes.

    As an added bonus I have also included an attached behaviour that allows the user to drag the Popup around using an embedded Thumb control. That attached behaviour is shown below.

    /// <summary>
    /// Allows moving of Popup using a Thumb
    /// </summary>
    public class PopupBehaviours
    {
        #region IsMoveEnabled DP
        public static Boolean GetIsMoveEnabledProperty(DependencyObject obj)
        {
            return (Boolean)obj.GetValue(IsMoveEnabledPropertyProperty);
        }
    
        public static void SetIsMoveEnabledProperty(DependencyObject obj, Boolean value)
        {
            obj.SetValue(IsMoveEnabledPropertyProperty, value);
        }
    
        // Using a DependencyProperty as the backing store for 
        //IsMoveEnabledProperty. 
        public static readonly DependencyProperty IsMoveEnabledPropertyProperty =
            DependencyProperty.RegisterAttached("IsMoveEnabledProperty",
            typeof(Boolean), typeof(PopupBehaviours), 
    		new UIPropertyMetadata(false,OnIsMoveStatedChanged));
    
    
        private static void OnIsMoveStatedChanged(DependencyObject sender, 
    		DependencyPropertyChangedEventArgs e)
        {
            Thumb thumb = (Thumb)sender;
    
            if (thumb == null) return;
    
            thumb.DragStarted -= Thumb_DragStarted;
            thumb.DragDelta -= Thumb_DragDelta;
            thumb.DragCompleted -= Thumb_DragCompleted;
    
            if (e.NewValue != null && e.NewValue.GetType() == typeof(Boolean))
            {
                thumb.DragStarted += Thumb_DragStarted;
                thumb.DragDelta += Thumb_DragDelta;
                thumb.DragCompleted += Thumb_DragCompleted;
            }
    
        }
        #endregion
    
        #region Private Methods
        private static void Thumb_DragCompleted(object sender, 
    		DragCompletedEventArgs e)
        {
            Thumb thumb = (Thumb)sender;
            thumb.Cursor = null;
        }
    
        private static void Thumb_DragDelta(object sender, 
    	DragDeltaEventArgs e)
        {
            Thumb thumb = (Thumb)sender;
            Popup popup = thumb.Tag as Popup;
    
            if (popup != null)
            {
                popup.HorizontalOffset += e.HorizontalChange;
                popup.VerticalOffset += e.VerticalChange;
            }
        }
    
        private static void Thumb_DragStarted(object sender, 
    	DragStartedEventArgs e)
        {
            Thumb thumb = (Thumb)sender;
            thumb.Cursor = Cursors.Hand;
        }
        #endregion
    
    }

    To use this attached behaviour the, Popup itself must use a particular Style this is shown below.

    <Popup x:Name="popLines"  
           PlacementTarget="{Binding ElementName=mainGrid}"
           Placement="Relative"
           IsOpen="False"
           Width="400" Height="225"
           AllowsTransparency="True"
           StaysOpen="False"
           PopupAnimation="Scroll"
           HorizontalAlignment="Right"
           HorizontalOffset="30" VerticalOffset="30" >
    
        <Border Background="Transparent" HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"
                BorderBrush="#FF000000" 
                BorderThickness="3" 
                CornerRadius="5,5,5,5">
    
            <Grid Background="White">
    
                <Grid.RowDefinitions>
                    <RowDefinition Height="40"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
    
                <Thumb Grid.Row="0" Width="Auto" Height="40" 
                       Tag="{Binding ElementName=popLines}"
                       local:PopupBehaviours.IsMoveEnabledProperty="true">
                    <Thumb.Template>
                        <ControlTemplate>
                            <Border  Width="Auto" Height="40" 
    				 BorderBrush="#FF000000" 
                                     Background="Black" VerticalAlignment="Top" 
                                     CornerRadius="5,5,0,0" Margin="-2,-2,-2,0">
    
                                <Label Content="Lines"
                                   FontSize="18"
                                   FontWeight="Bold"
                                   Foreground="White"
                                   VerticalContentAlignment="Center"
                                   Margin="5,0,0,0" />
                            </Border>
                        </ControlTemplate>
                    </Thumb.Template>
                </Thumb>
    
                <!-- Actual Content Grid-->
                <Grid Grid.Row="1" Background="White"
                          Width="Auto"  Height="Auto" Margin="0,0,0,10">
                    <ScrollViewer HorizontalScrollBarVisibility="Hidden"
    		      VerticalScrollBarVisibility="Visible">
      
    
                    </ScrollViewer> 
    
                </Grid>
    
    
            </Grid>
        </Border>
    </Popup>
    

    Textual Results

    All along I knew I wanted to create a jazzy popup window, that would hold the textual description of the solution path found, that would allow the user to read the description in english. The idea is simple for each StationViewModel on the SolutionFound SearchPath, iterate through and build up a text description of the route and display it to the user. This is done in the actual SearchPath object as follows:

    private String GetPathDescription()
    {
        StringBuilder thePath = new StringBuilder(1000);
        StationViewModel currentStation;
    
        for (int i = 0; i < this.StationsOnPath.Count - 1; i++)
        {
            IList<Line> otherLines = this.StationsOnPath[i + 1].Lines;
            List<Line> linesInCommon = 
                this.StationsOnPath[i].Lines.Intersect(otherLines).ToList();
            if (i == 0)
            {
                currentStation = this.StationsOnPath[0];
                thePath.Append("Start at " + currentStation.Name +
                    " station, which is on the " + 
                    NormaliseLineName(linesInCommon[0]) + " line, ");
            }
            else
            {
                currentStation = this.StationsOnPath[i];
                thePath.Append("\r\nThen from " + currentStation.Name +
                    " station, which is on the " + 
                    NormaliseLineName(linesInCommon[0]) + " line, ");
    
            }
    
            thePath.Append("take the " + NormaliseLineName(linesInCommon[0]) +
                " line to " + this.StationsOnPath[i + 1] + " station\r\n");
        }
    
        //return the path description
        return thePath.ToString();
    }
    

    So after that all that is left to do is display it. I talked about how I do that on my blog the other day, http://sachabarber.net/?p=580. The basic idea is that you use a Popup and get it to support Transparency and then draw a callout graphic Path, and show the SearchPath text.

    Here is in action, I think it looks pretty cool.

    This is all done using the following XAML

    <Popup x:Name="popInformation"  
           PlacementTarget="{Binding ElementName=imgInformation}"
           Placement="Top"
           IsOpen="False"
           Width="400" Height="250"
           AllowsTransparency="True"
           StaysOpen="True"
           PopupAnimation="Scroll"
           HorizontalAlignment="Right"
           VerticalAlignment="Top"
           HorizontalOffset="-190" VerticalOffset="-10" >
        <Grid Margin="10">
            <Path Fill="LightYellow" Stretch="Fill" 
                  Stroke="LightGoldenrodYellow" 
    	            StrokeThickness="3" StrokeLineJoin="Round"
    	            Margin="0" Data="M130,154 L427.5,154 427.5,
                  240.5 299.5,240.5 287.5,245.5 275.5,240.5 130,240.5 z">
                <Path.Effect>
                    <DropShadowEffect BlurRadius="12" Color="Black" 
                                      Direction="315" Opacity="0.8"/>
                </Path.Effect>
            </Path>
    
            <Grid Height="225" Margin="10,5,10,5"
                  HorizontalAlignment="Stretch" VerticalAlignment="Top">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                
                
                <Label Grid.Row="0" FontSize="16" 
                       FontWeight="Bold" Content="Your Route" 
                       HorizontalAlignment="Left" Margin="0,5,5,5"
                       VerticalAlignment="Center"/>
                
                
                <Button Grid.Row="0" HorizontalAlignment="Right"
                        Width="25" Click="Button_Click"
                        Height="25" VerticalAlignment="Top" Margin="2">
                    <Button.Template>
                        <ControlTemplate TargetType="{x:Type Button}">
                            <Border x:Name="bord" CornerRadius="3" 
                                    BorderBrush="Transparent" 
                                    BorderThickness="2"
                                    Background="Transparent" 
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center" 
                                    Margin="0" Width="25" Height="25">
                                <Label x:Name="lbl" Foreground="Black" 
                                	   FontWeight="Bold" 
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center"
                                       FontFamily="Wingdings 2" Content="O"  
                                       FontSize="14"/>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsMouseOver" 
                                Value="True">
                                    <Setter TargetName="bord" 
                                            Property="BorderBrush" 
                                            Value="Black"/>
                                    <Setter TargetName="bord" 
                                            Property="Background"
                                             Value="Black"/>
                                    <Setter TargetName="lbl" 
                                            Property="Foreground" 
                                            Value="LightGoldenrodYellow"/>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Button.Template>
                    
                </Button>
                
                <ScrollViewer Grid.Row="1" Margin="0,5,0,30"
                              HorizontalScrollBarVisibility="Disabled" 
                              VerticalScrollBarVisibility="Auto">
                    <TextBlock  TextWrapping="Wrap"  FontSize="10" Margin="5"
                           VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
    	            Text="{Binding Path=SearchPathDescription}"/>
                </ScrollViewer>
    
            </Grid>
    
    
        </Grid>
    </Popup>
    

    That's It Hope You Liked It

    Anyway there you go, hope you liked it. Even if you dont have a use for a London tube planner I think there is still lots of good WPF learning material in this one

    Thanks.

    As always votes / comments are welcome.

    You must Sign In to use this message board.
     
     
    Per page   
     FirstPrevNext
    GeneralWonderful
    Marcelo Ricardo de Oliveira
    6:25 7 Dec '09  
    Another masterpiece, 5 stars indeed... thanks for sharing, Sacha

    Take a look at full source code C# Snooker game here in Code Project.

    GeneralRe: Wonderful
    Sacha Barber
    7:14 7 Dec '09  
    Thanks man, Good luck with your Snooker article (smells like a winner to me)

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRe: Wonderful
    Marcelo Ricardo de Oliveira
    10:10 7 Dec '09  
    Thanks! (but I think yours is better)

    Take a look at full source code C# Snooker game here in Code Project.

    GeneralRe: Wonderful
    Sacha Barber
    10:27 7 Dec '09  
    The article votes tell a different story

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralBad Performance
    theasavcuba
    8:13 17 Nov '09  
    Why this wpf aplication have so bad performance.
    Am running on a 3.0Ghz, 3.0Ghz, 1 Gb RAM. and the panning is so slow...
    GeneralRe: Bad Performance
    Sacha Barber
    11:08 17 Nov '09  
    I honestly don't know my work machine does that as well but at home my spec it way less that yours and it flies. Graphics card maybe

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRe: Bad Performance
    theasavcuba
    16:45 17 Nov '09  
    It seems, a huge diference when the wpf let the cpu to do the work...
    GeneralRe: Bad Performance
    Sacha Barber
    22:44 17 Nov '09  
    Yeah its truly sad that one has to have such a good graphics card

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralMasterpiece indeed
    Raul Mainardi Neto
    1:05 13 Nov '09  
    Have another 5 mate, was missing to read your stuff... dont keep us waiting so much...

    All best
    GeneralRe: Masterpiece indeed
    Sacha Barber
    1:38 13 Nov '09  
    Cheers mate

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRepetitively Boring
    Member 4565433
    1:30 12 Nov '09  
    Not your posting.....just another 5 from me.
    Can I automate my voting, so that I open an article of yours and auto mark of 5 is invoked.

    Coming from an 18 year embedded real-time C/C++ background and over the last couple of years windowing via WPF (graphical control systems for the C stuff).
    I’ve found your articles a great help, along with Josh Smith, Daniel Vaughan etc.

    Thanks for sharing
    GeneralRe: Repetitively Boring
    Sacha Barber
    1:49 12 Nov '09  
    Yeah Daniel/Karl/Pete O'Hanlon /Marlon Grech/Josh are all on it. Those guys are too cool for school

    Thanks for the 5 man

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralLove it!
    Philip F.
    3:00 10 Nov '09  
    You really are inspiring. Great article!
    Phil

    I won’t not use no double negatives.

    GeneralRe: Love it!
    Sacha Barber
    3:20 10 Nov '09  
    Thanks man

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralAnother masterpiece !
    BillWoodruff
    23:58 9 Nov '09  
    Applause !

    best, Bill

    "Many : not conversant with mathematical studies, imagine that because it [the Analytical Engine] is to give results in numerical notation, its processes must consequently be arithmetical, numerical, rather than algebraical and analytical. This is an error. The engine can arrange and combine numerical quantities as if they were letters or any other general symbols; and it fact it might bring out its results in algebraical notation, were provisions made accordingly." Ada, Countess Lovelace, 1844

    GeneralRe: Another masterpiece !
    Sacha Barber
    1:31 10 Nov '09  
    Thanks bill

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralA* algorithm seriously flawed
    Emile van Gerwen
    11:10 9 Nov '09  
    The A* algorithm as presented in the article will not calculate the shortest path.

    As an example: plan a route from Charing Cross to Stockwell: according to the planner you will need to change lines twice, while you could travel on the Norhern line all the way.

    The problem is that the heuristic function is not admissible: the distance to the end station is not necessarily lower than the cost (number of line changes). In fact, these are expressed in different units.

    -Emile.
    GeneralRe: A* algorithm seriously flawed
    Sacha Barber
    22:51 9 Nov '09  
    One of my team mates just said the same, but according to my uni notes, this is how we did it back then, so I just did the same but prettied it up. Too be honest it was for fun, so I am done with it.

    Thats is also why I said A* like algorithm.

    Thanks for your comments though.

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRe: A* algorithm seriously flawed
    Emile van Gerwen
    23:15 9 Nov '09  
    It is not difficult to get it correct. The WPF part of the article is good, but since A* is so prominently in the title, readers may expect a solid explanation and correct implementation of the A* algorithm. My suggestion would be to change the title in "Tube planner in WPF" or something.
    GeneralRe: A* algorithm seriously flawed
    Sacha Barber
    23:40 9 Nov '09  
    If you have a suggestion re: HCost that you think would work better, Ill try that. Too be honest Emile, this one took a while so unless its something small I will more than likely let it slide. Sadly, but true, I just want to move on. I hear what you are saying though, but find it hard to think that all my old notes are wrong, but I guess as you say it should be admissable. Maybe using zone would be a better idea.

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRe: A* algorithm seriously flawed
    Emile van Gerwen
    7:20 11 Nov '09  
    I think part of the misunderstanding comes from the misleading use of "cost" and "distance" in the various sources of information.

    My suggestion would be as follows.

    Suppose we are interested to find the path with minimal travel time. And suppose travel time is related to straight line distance between stations, the number of stations we pass along the way (because train has to stop there), and line changes.

    Then:

    GCost = DistanceBetween(previousStation, station) * TIME_PER_DISTANCE_UNIT
    + 1 * TIME_PER_STATION_PASSAGE
    if (station not on same line as previous station)
    GCost += TIME_FOR_LINE_CHANGE;

    HCost = DistanceBetween(station, endStation) * TIME_PER_DISTANCE_UNIT;


    You can easily see that HCost is admissible because any path from the station to endStation is always at least as long as the straight line to the end station. You could improve efficiency by adding TIME_FOR_LINE_CHANGE if station is not on the same line as endStation because then you would need at least one line change.

    (For this to be correct you'll have to calculate distance as Sqrt(sqr(x1-x2)+sqr(y1-y2)) and not using the absolute values)

    -Emile
    GeneralRe: A* algorithm seriously flawed
    Sacha Barber
    23:08 9 Nov '09  
    Actually,

    Emile if you can think of a good (in your opinion) HCost that is easy to implement, I'll do it. But as far as my old notes go and my lecturers examples go, I feel I have done it right. Which I am open to admitting could be wrong.

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRe: A* algorithm seriously flawed
    Roger Alsing
    1:11 10 Nov '09  
    >>The A* algorithm as presented in the article will not calculate the shortest path.

    A* is not designed to find the shortest path, its designed to find a short path w/o searching the entire problem space..
    It's an optimization so you can get decent pathfinding in games etc, its not a flaw.

    Use Dijkstra's algorithm if you want shortest path... but it is way more CPU intensive.

    Sacha, you could make a second article showing the same app using different graph search algorithms using strategy pattern Smile
    And show path length and how long it took to calculate it in the gui.


    GeneralRe: A* algorithm seriously flawed
    Sacha Barber
    1:30 10 Nov '09  
    Ah so its ok then, either way I dont really want to change this, as I am tired of it

    Sacha Barber
    • Microsoft Visual C# MVP 2008/2009
    • Codeproject MVP 2008/2009
    Your best friend is you.
    I'm my best friend too. We share the same views, and hardly ever argue

    My Blog : sachabarber.net

    GeneralRe: A* algorithm seriously flawed
    Emile van Gerwen
    7:03 11 Nov '09  
    A* is designed to find the optimal path, but for that you have to have an admissible h-cost.

    Quote from the article, which in turn quotes wikipedia:

    In computer science, A* (pronounced "A star") is a best-first graph search algorithm that finds the least-cost path from a given initial node to one goal node
    [...]
    The h(x) part of the f(x) function must be an admissible heuristic


    By choosing a non-admissible h-cost you would indeed make an approximation but you should be very careful.

    In the article the heuristic is choosen very badly in relation to the g-cost, so I would not be suprised if this A* implementation is degraded to a greedy search.


    Last Updated 8 Nov 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010