So far we have built an earthquake application, mapped an address using geocoding, and even Prism-fied the Bing Maps WPF control. Now let’s see how to map a route with turn-by-turn directions. By now it should be no surprise that you will need to complete the following steps before you can create this application.
You MUST have a Bing Maps API key in order to use any of the SOAP services.
For this application we will be using the Geocode Service and the Route Service. The Geocode service is used to match addresses, places, and geographic entities to latitude and longitude coordinates on the map, as well as return location information for a specified latitude and longitude coordinate. The Route service is used to generate routes and driving directions based on locations or waypoints.
Now that you have that out of the way let’s write an application. Create a new WPF application targeting the .NET 4.0 framework. Add a reference to the Microsoft.Maps.MapControl.WPF.dll. This will most likely be located in Program Files or Program Files (x86) –> Bing Maps WPF Control –> Beta –> Libraries.
Open up your App.xaml. You need to add an ApplicationIdCredentialsProvider as a resource and enter your ApplicationId:
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:bing=”clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF”
StartupUri=”MainWindow.xaml”>
<Application.Resources>
<bing:ApplicationIdCredentialsProvider x:Key=”MyCredentials” ApplicationId=”Your API Key” />
</Application.Resources>
</Application>
We need to add the Geocode and Route service to our application. First, let’s start by adding the Geocode service. Right click the project and select “Add Service Reference”. Use the following address:
http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc
Give your service a name. I called mine BingServices. Now we need to add the Route service, but we don’t want to add a separate service reference. Ideally we would have a single service that contained both the Geocode and Route services. Luckily there is a trick we can use to do just that. Select the Project and click the “Show All Files” button at the top of the Solution Explorer. Expand the Service Reference folder and then expand the BingServices service. Look for a file called Reference.svcmap. Open the References.svcmap and locate the MetadataSource node. It will loko something like this:
<MetadataSourceAddress=“http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc/mex“Protocol=“mex“SourceId=“1“ />
</MetadataSources>
Simply add a new MetatdataSource as follows:
<MetadataSourceAddress=“http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc/mex“Protocol=“mex“SourceId=“1“ />
<MetadataSourceAddress=“http://dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc/mex“Protocol=“mex“SourceId=“2“ />
</MetadataSources>
As you can see we added a new MetadataSource that points to the Route service and we gave it a SourceId of 2. Now right click the service and select “Update Service Reference”. This will generate a single service proxy for both the Geocode and Route services. Pretty slick huh?
The next step will be to create a ViewModel that will contain an ICommand that will execute the route calculation, properties that will represent a “To” address and a “From” address. We will also need a property for the RouteResult and for the directions we generate. Lets first create our Direction object:
{
public string Description { get; set; }
public Location Location { get; set; }
}
Now we will need a make-shift delegate command (do not use in production code):
{
private Action _execute;
public DelegateCommand(Action execute)
{
_execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_execute.Invoke();
}
}
Now on to the ViewModel itself:
{
public ICommand CalculateRouteCommand { get; private set; } private string _from = “Meridian, ID”;
public string From
{
get { return _from; }
set
{
_from = value;
OnPropertyChanged(“From”);
}
}
private string _to = “Boise, ID”;
public string To
{
get { return _to; }
set
{
_to = value;
OnPropertyChanged(“To”);
}
}
private BingServices.RouteResult _routeResult;
public BingServices.RouteResult RouteResult
{
get { return _routeResult; }
set
{
_routeResult = value;
OnPropertyChanged(“RouteResult”);
}
}
private ObservableCollection<Direction> _directions;
public ObservableCollection<Direction> Directions
{
get { return _directions; }
set
{
_directions = value;
OnPropertyChanged(“Directions”);
}
}
public RouteViewModel()
{
CalculateRouteCommand = new DelegateCommand(CalculateRoute);
}
private void CalculateRoute()
{
var from = GeocodeAddress(From);
var to = GeocodeAddress(To);
CalculateRoute(from, to);
}
private BingServices.GeocodeResult GeocodeAddress(string address)
{
BingServices.GeocodeResult result = null;
using (BingServices.GeocodeServiceClient client = new BingServices.GeocodeServiceClient(“CustomBinding_IGeocodeService”))
{
BingServices.GeocodeRequest request = new BingServices.GeocodeRequest();
request.Credentials = new Credentials() { ApplicationId = (App.Current.Resources[“MyCredentials”] as ApplicationIdCredentialsProvider).ApplicationId };
request.Query = address;
result = client.Geocode(request).Results[0];
}
return result;
}
private void CalculateRoute(BingServices.GeocodeResult from, BingServices.GeocodeResult to)
{
using (BingServices.RouteServiceClient client = new BingServices.RouteServiceClient(“CustomBinding_IRouteService”))
{
BingServices.RouteRequest request = new BingServices.RouteRequest();
request.Credentials = new Credentials() { ApplicationId = (App.Current.Resources[“MyCredentials”] as ApplicationIdCredentialsProvider).ApplicationId };
request.Waypoints = new ObservableCollection<BingServices.Waypoint>();
request.Waypoints.Add(ConvertResultToWayPoint(from));
request.Waypoints.Add(ConvertResultToWayPoint(to));
request.Options = new BingServices.RouteOptions();
request.Options.RoutePathType = BingServices.RoutePathType.Points;
RouteResult = client.CalculateRoute(request).Result;
}
GetDirections();
}
private void GetDirections()
{
Directions = new ObservableCollection<Direction>();
foreach (BingServices.ItineraryItem item in RouteResult.Legs[0].Itinerary)
{
var direction = new Direction();
direction.Description = GetDirectionText(item);
direction.Location = new Location(item.Location.Latitude, item.Location.Longitude);
Directions.Add(direction);
}
}
private static string GetDirectionText(BingServices.ItineraryItem item)
{
string contentString = item.Text;
//Remove tags from the string
Regex regex = new Regex(“<(.|n)*?>”);
contentString = regex.Replace(contentString, string.Empty);
return contentString;
}
private BingServices.Waypoint ConvertResultToWayPoint(BingServices.GeocodeResult result)
{
BingServices.Waypoint waypoint = new BingServices.Waypoint();
waypoint.Description = result.DisplayName;
waypoint.Location = result.Locations[0];
return waypoint;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Let’s walk though the CalculateRoute Method. First we geocode the “From” and “To” properties the same way we did in the geocoding post. Once we have our From and To locations we use those results to generate a RouteRequest and send that request to the Route service. The service will generate a result and we set our RouteResult property accordingly. This property will be used in data binding shortly. Next we generate an ObservableCollection<Direction> by looping through all the ItineraryItems in the result.
Now that we have generate all of our data, we need to create a View that supports and displays this data, but before we do that we need to create some attached properties. Why do you ask? Well because as it stands, the Bing Maps WPF control is not data binding friendly. So we need to enable data binding with the use of attached properties. We will need an attached property for the Route result which will be responsible for drawing the route line on the map. We will also need one for the MapLayer that will be used to contain the route line. Here is the code:
{
#regionRouteResult public static readonly DependencyProperty RouteResultProperty = DependencyProperty.RegisterAttached(“RouteResult”, typeof(BingServices.RouteResult), typeof(MapInteractivity), new UIPropertyMetadata(null, OnRouteResultChanged));
public static BingServices.RouteResult GetRouteResult(DependencyObject target)
{
return (BingServices.RouteResult)target.GetValue(RouteResultProperty);
}
public static void SetRouteResult(DependencyObject target, BingServices.RouteResult value)
{
target.SetValue(RouteResultProperty, value);
}
private static void OnRouteResultChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
OnRouteResultChanged((Map)o, (BingServices.RouteResult)e.OldValue, (BingServices.RouteResult)e.NewValue);
}
private static void OnRouteResultChanged(Map map, BingServices.RouteResult oldValue, BingServices.RouteResult newValue)
{
MapPolyline routeLine = new MapPolyline();
routeLine.Locations = new LocationCollection();
routeLine.Opacity = 0.65;
routeLine.Stroke = new SolidColorBrush(Colors.Blue);
routeLine.StrokeThickness = 5.0;
foreach (BingServices.Location loc in newValue.RoutePath.Points)
{
routeLine.Locations.Add(new Location(loc.Latitude, loc.Longitude));
}
var routeLineLayer = GetRouteLineLayer(map);
if (routeLineLayer == null)
{
routeLineLayer = new MapLayer();
SetRouteLineLayer(map, routeLineLayer);
}
routeLineLayer.Children.Clear();
routeLineLayer.Children.Add(routeLine);
//Set the map view
LocationRect rect = new LocationRect(routeLine.Locations[0], routeLine.Locations[routeLine.Locations.Count – 1]);
map.SetView(rect);
}
#endregion //RouteResult
#region RouteLineLayer
public static readonly DependencyProperty RouteLineLayerProperty = DependencyProperty.RegisterAttached(“RouteLineLayer”, typeof(MapLayer), typeof(MapInteractivity), new UIPropertyMetadata(null, OnRouteLineLayerChanged));
public static MapLayer GetRouteLineLayer(DependencyObject target)
{
return (MapLayer)target.GetValue(RouteLineLayerProperty);
}
public static void SetRouteLineLayer(DependencyObject target, MapLayer value)
{
target.SetValue(RouteLineLayerProperty, value);
}
private static void OnRouteLineLayerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
OnRouteLineLayerChanged((Map)o, (MapLayer)e.OldValue, (MapLayer)e.NewValue);
}
private static void OnRouteLineLayerChanged(Map map, MapLayer oldValue, MapLayer newValue)
{
if (!map.Children.Contains(newValue))
map.Children.Add(newValue);
}
#endregion //RouteLineLayer
}
The RouteResult property simply creates a MapPolyline, loops through all the points in the result and adds the locations to the route line. It then gets the RouteLineLayer and adds it to the map. Lastly, it sets the map’s view to best fit the route line.
Now we are ready for the View:
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:bing=”clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF”
xmlns:core=”clr-namespace:BingMapsCalculateRouteDemo.Core”
Title=”MainWindow” Height=”600″ Width=”800″> <Window.Resources>
<DataTemplate x:Key=”RouteTemplate”>
<Ellipse Width=”12″ Height=”12″ Fill=”Red” Opacity=”0.8″
bing:MapLayer.Position=”{Binding Location}“
bing:MapLayer.PositionOrigin=”Center”
Tag=”{Binding}“
MouseEnter=”Route_MouseEnter”
MouseLeave=”Route_MouseLeave”/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto” />
<ColumnDefinition Width=”*” />
</Grid.ColumnDefinitions>
<StackPanel MinWidth=”150″ Margin=”10″>
<TextBox Text=”{Binding From}” />
<TextBox Text=”{Binding To}“/>
<Button Content=”Get Route” Command=”{Binding CalculateRouteCommand}“/>
</StackPanel>
<bing:Map Grid.Column=”1″ Mode=”AerialWithLabels” HorizontalAlignment=”Stretch” VerticalAlignment=”Stretch” AnimationLevel=”Full”
core:MapInteractivity.RouteResult=”{Binding RouteResult}“
core:MapInteractivity.RouteLineLayer=”{Binding ElementName=RouteLineLayer}”
CredentialsProvider=”{StaticResource MyCredentials}” >
<bing:MapLayer x:Name=”RouteLineLayer” />
<bing:MapLayer>
<bing:MapItemsControl ItemsSource=”{Binding Directions}“
ItemTemplate=”{StaticResource RouteTemplate}“/>
</bing:MapLayer>
<bing:MapLayer x:Name=”ContentPopupLayer”>
<Grid x:Name=”ContentPopup” Visibility=”Collapsed” Background=”White”>
<StackPanel Margin=”15″>
<TextBlock x:Name=”ContentPopupText” FontSize=”12″ FontWeight=”Bold” TextWrapping=”Wrap” />
</StackPanel>
</Grid>
</bing:MapLayer>
</bing:Map>
</Grid>
</Window>
Oh and don’t forget the code behind:
{
public MainWindow()
{
InitializeComponent();
DataContext = new RouteViewModel();
} private void Route_MouseEnter(object sender, MouseEventArgs e)
{
FrameworkElement pin = sender as FrameworkElement;
MapLayer.SetPosition(ContentPopup, MapLayer.GetPosition(pin));
MapLayer.SetPositionOffset(ContentPopup, new Point(20, -15));
var location = (Direction)pin.Tag;
ContentPopupText.Text = location.Description;
ContentPopup.Visibility = Visibility.Visible;
}
private void Route_MouseLeave(object sender, MouseEventArgs e)
{
ContentPopup.Visibility = Visibility.Collapsed;
}
}
Now let’s view the fruits of our labor at runtime.
As you can see, given a “From” and “To” address, we can draw a route line and provide turn-by-turn directions as the user hovers over the direction pins. Pretty neat huh? And it wasn’t too difficult to pull off. As always you can Download the Source and start playing.