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 assignments we had was to create a search over a cut down list of the London underground 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 ends. Bummer, back to work.

So I thought I needed something fun to get myself back into work mode, and 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 an 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.

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 screenshots. And here is the list of the highlights:

Panning

Zooming

Station connection tooltips

Hiding unwanted lines (to de-clutter the diagram)

Information bubble

Diagrammatic solution path visualization

Textual solution path description

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.

Line connections: which I pain stakingly hand crafted myself in an XML file called "StationConnections.xml".

In order to load this data, these two 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 which has a ViewModel called PlannerViewModel set as its 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.

Where the state object being passed to the worker is an instance of the main PlannerViewModel that is being used by the MainWindowDataContext. The PlannerViewModel is 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 StationControl is positioned within the TubeStationsCanvas using some DPs.

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 retrieved 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));
}
}

As I just mentioned, there is a specialized Canvas control called TubeStationsCanvas whose job it is to render the station connections and the search solution path. We just covered how the stations are actually loaded in the first place, so how do the connections 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. Let's 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:

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.

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

Zooming, well, credit where credit is due, is more than likely sourced for all zooming WPF apps from Vertigo's sublime initial WPF exemplar, 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, zooming is realized using a standard WPF Slider control (albeit I have jazzed it up a bit with a XAML Style) and a ScaleTransform.

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 StationControl's 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><BorderHorizontalAlignment="Stretch"SnapsToDevicePixels="True"BorderBrush="Black"Background="White"VerticalAlignment="Stretch"Height="Auto"BorderThickness="3"CornerRadius="5"><GridMargin="0"Width="300"><Grid.RowDefinitions><RowDefinitionHeight="40"/><RowDefinitionHeight="Auto"/></Grid.RowDefinitions><BorderCornerRadius="2,2,0,0"Grid.Row="0"BorderBrush="Black"BorderThickness="2"Background="Black"><StackPanelOrientation="Horizontal"><ImageSource="../Images/tubeIconNormal.png"Width="30"Height="30"VerticalAlignment="Center"HorizontalAlignment="Left"Margin="2"/><LabelContent="{Binding DisplayName}"FontSize="14"FontWeight="Bold"Foreground="White"VerticalContentAlignment="Center"Margin="5,0,0,0"/></StackPanel></Border><StackPanelOrientation="Vertical"Grid.Row="1"><!-- Bakerloo --><StackPanelOrientation="Horizontal"HorizontalAlignment="Stretch"Visibility="{Binding RelativeSource={RelativeSource Self},
Path=DataContext, Converter={StaticResource OnLineToVisibilityConv},
ConverterParameter='Bakerloo'}"><BorderBorderBrush="Black"BorderThickness="2"CornerRadius="5"Background="Brown"Height="30"Width="30"Margin="10,2,10,2"><ImageSource="../Images/tubeIcon.png"Width="20"Height="20"HorizontalAlignment="Center"VerticalAlignment="Center"/></Border><LabelContent="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 --><LabelContent="{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.xamlResourceDictionary which dictates what ToolTips should look like. Here is that XAML:

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.

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 path-cost function, which is the cost from the starting node to the current node (usually denoted g(x)).

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

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).

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.

FCost

This is the magic figure that guides the search when the SearchPaths in the Agenda are sorted. 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 is similar to what we saw in drawing 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.

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 screenshot of the line visibility popup and the effect 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:

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:

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.

I really like your article, especially your visualization. At university, I experimentalized with A*, too. And at Microsofts Imagine-Cup, where I used A* to find a the shortest paths on a 200x200 node map.