Multiple Measurement Scale Bar in WPF

I recently had some one ask me how to create a custom scale bar in the Bing Maps WPF control. The scale bars in the WPF control today let you set the unit of measurement to imperial or metric which is great if you only want one unit of measurement. However, if you are used to using the Bing Maps V7 AJAX control you might have noticed that control has two scale bars, an imperia one and a metric one. In this blog post we are going to see how to create a custom scale bar that shows both imperial and metric information.

Full source code for this blog post can be downloaded from the MSDN Code Gallery here.

Setting up the Visual Studio Project

To get started create a WPF Application project in Visual Studios called CustomScalebar. Once this is done add a reference to the Bing Maps WPF library (Microsoft.Maps.MapControl.WPF). If you don’t already have the Bing Maps WPF SDK installed you can download it here.

Next open the MainWindow.xaml file and update it with the following XAML. This will add a Bing Maps control to the page.

 <Window x:Class="CustomScalebar.MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:m="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <m:Map Name="MyMap" CredentialsProvider="YOUR_BING_MAPS_KEY"/>
    </Grid>
</Window>

Creating the custom scale bar control

Create a new class file in the project called MultiValueScalebar.cs. We will make this class inherit from the Grid class and add all the components of the scale bar as children of this class. When the MultiValueScalebar class is initialized it will take in a reference to a map control. It will then hide the default scale bar on the map and create some text boxes for displaying the labels of the scale bar and a couple of rectangles that will represent the bars themselves. The ViewChangeOnFrame event of the map will be used to trigger a method to update the scale bar values and size. Update the MultiValueScalebar.cs file with the following code:

 using Microsoft.Maps.MapControl.WPF;
using System;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace CustomScalebar
{
    public class MultiValueScalebar : Grid
    {
        private Map _map;

        private Rectangle _metricScaleBar;
        private Rectangle _imperialScaleBar;

        private TextBlock _metricScaleValue;
        private TextBlock _imperialScaleValue;

        //A set of values in which to round the scale bars values off to.
        private double[] scaleMultipliers = new double[] { 1000, 500, 250, 200, 100, 50, 25, 10, 5, 2, 1, 0.5};

        public MultiValueScalebar(Map map)
        {
            _map = map;

            //Hide default scalebar.
            _map.ScaleVisibility = System.Windows.Visibility.Hidden;

            //Set initial size and position information for scale bar panel.
            this.Width = 250;
            this.Height = 30;
            this.HorizontalAlignment = System.Windows.HorizontalAlignment.Right;
            this.VerticalAlignment = System.Windows.VerticalAlignment.Bottom;
            this.Margin = new System.Windows.Thickness(0,0,10,30);

            //Create the metric scalebar and label.
            _metricScaleValue = new TextBlock()
            {
                Text = "1000 km",
                HorizontalAlignment = System.Windows.HorizontalAlignment.Right
            };

            this.Children.Add(_metricScaleValue);

            _metricScaleBar = new Rectangle(){
                Fill = new SolidColorBrush(Colors.DodgerBlue),
                Stroke = new SolidColorBrush(Colors.Black),
                StrokeThickness = 1,
                HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
                Width = 200,
                Height = 6,
                Margin = new System.Windows.Thickness(0, 20, 0, 0)
            };

            this.Children.Add(_metricScaleBar);

            //Create the imperial scalebar and label.
            _imperialScaleValue = new TextBlock()
            {
                Text = "1000 miles",
                HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
                Margin = new System.Windows.Thickness(0, 0, 110, 0)
            };

            this.Children.Add(_imperialScaleValue);

            _imperialScaleBar = new Rectangle()
            {
                Fill = new SolidColorBrush(Colors.DodgerBlue),
                Stroke = new SolidColorBrush(Colors.Black),
                StrokeThickness = 1,
                HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
                Width = 200,
                Height = 6,
                Margin = new System.Windows.Thickness(0, 20, 110, 0)
            };

            this.Children.Add(_imperialScaleBar);

            //Add a viewchange event to update the scale bar.
            map.ViewChangeOnFrame += (s, e) =>
            {
                UpdateScalebar();
            };

            //Add this scalebar to the map.
            map.Children.Add(this);

            //Update the scale bar for the current map view.
            UpdateScalebar();
        }

        private void UpdateScalebar()
        {
           // Update scale bars.
        }
    }
}

In order to calculate the dimensions of the scale bars we need to know the ground resolution at the center of the map. There is a good MSDN article on how to calculate this here. Add the following method to the MultiValueScalebar class. It will calculate the ground resolution for a given latitude value in km/pixel.

 public double GroundResolution(double latitude, int zoom)
{
    return Math.Cos(latitude * Math.PI / 180) * 2 * Math.PI *  6378.135 / (Math.Pow(2, zoom) * 256);
}

Finally, we need to add some logic to the UpdateScalebar method. In this method we will calculate the ground resolution for the center of the map and then use this to determine the distance the scale bar would represent if it were 100 pixels wide. If the distance is less than 1 then the unit of measure meant for the scale bar will be changed from km or miles, to meters or feet respectively. We will then loop though an array of values that are used to round off the distance to a nice value. The rounded off distance is then used to calculate the width of the scale bare in pixels. This is done for both the imperial and metric scale bars. To do all this update the UpdateScalebar method with the following code.

 private void UpdateScalebar()
{
    //Calculate the ground resolution in km/pixel based on the center of the map and current zoom level.
    var metricResolution = GroundResolution(_map.Center.Latitude, (int)Math.Round(_map.ZoomLevel));
    var imperialResolution = metricResolution * 0.62137119; //KM to miles

    double maxScaleBarWidth = 100;

    string metricUnitName = "km";
    double metricDistance = maxScaleBarWidth * metricResolution;

    if (metricDistance < 1)
    {
        metricUnitName = "m";
        metricDistance *= 1000;
        metricResolution *= 1000;
    }

    for (var i = 0; i < scaleMultipliers.Length; i++)
    {
        if (metricDistance / scaleMultipliers[i] > 1)
        {
            var scaleValue = metricDistance - metricDistance % scaleMultipliers[i];
            _metricScaleValue.Text = string.Format("{0:F0} {1}", scaleValue, metricUnitName);
            _metricScaleBar.Width = scaleValue / metricResolution;
            break;
        }
    }
            
    string imperialUnitName = "miles";
    double imperialDistance = maxScaleBarWidth * imperialResolution;
            
    if (imperialDistance < 1)
    {
        imperialUnitName = "feet";
        imperialDistance *= 5280;
        imperialResolution *= 5280;
    }

    for (var i = 0; i < scaleMultipliers.Length; i++)
    {
        if (imperialDistance / scaleMultipliers[i] > 1)
        {
            var scaleValue = imperialDistance - imperialDistance % scaleMultipliers[i];
            _imperialScaleValue.Text = string.Format("{0:F0} {1}", scaleValue, imperialUnitName);
            _imperialScaleBar.Width = scaleValue / imperialResolution;
            break;
        }
    }
}

Implementing the Custom Scale Bar

Implementing the MultiValueScalebar class is fairly easy. We simply need to create an instance of the class once when the page loads. Open the MainWindow.xaml.cs file and update it with the following code:

 using System.Windows;

namespace CustomScalebar
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            new MultiValueScalebar(MyMap);
        }
    }
}

If you now run the application you will see the new scale bar. For reference here is a screenshot of how the scale bar looks normally:

image

Here is a screenshot of how the custom scale bar looks:

image

If you would like you can easily reposition the scale bar using the HortizontalAlignment, VerticalAlignment and Margin properties. For example, the following code will display the scale bar in the top right corner of the map.

 new MultiValueScalebar(MyMap)
{
    VerticalAlignment = System.Windows.VerticalAlignment.Top
};

As mentioned at the start of this blog post full source code for this blog post can be downloaded from the MSDN Code Gallery here.