Skip to main content

Drawing shapes on top of Bing Maps in a WP7 app

Before I complete the 'How many pins can Bing Maps handle in a WP7 app...' set of posts. I wanted to show how I'm drawing shapes on top of the Bing Maps control in WP7. This is based around using the MapPolygon class in the Microsoft.Phone.Controls.Maps namespace, more info on MSDN. Basically this will drawn lines betweens the geo-locations defined in the collection exposed by the Locations property. You're also able to define other properties such as fill colour, stroke thinkness, opacity. With these you have the ability to really customize any polygon you render over the map control.

I'm going to show how I achieved the following screen shots and how this is all based around using the well known Haversine formula with only the centre location of the visible bounding rectangle of the Bing Maps control:


A couple of things to note, UI design is not my forte (as @RichGee will tell you) so the following is more about how to achieve it than what it finally looks like and secondly I'm using the Haversine formula to calculate geo-locations. This formula is not the most accurate available, but for my purposes the 0.3% error factor is acceptable. 'Movable Type' has a great page about lat & long formulas and calculations, more info here.

I've used MVVM pattern for this app so therefore I have a set of Model classes that are bounded to the View via the ViewModel. The app only has 1 View, 1 ViewModel and 1 Model class but the Model class uses both object orientated and functional techniques to achieve what I wanted.

The View has the map control and polygon defined  in XAML as follows:

As you can see the map Centre property is bound to the ViewModel and the Locations property of the MapPolygon is bound to the Polygon geo-locations collection on the ViewModel.

The Shape Model class defines a couple of properties, one for the shape name and the second a function for calculating the geo-locations used to describe the polygon (shape). This is a function delegate because the geo-locations are generated dynamically at runtime and this depends on the current centre location of the map control.

public sealed class Shape : BaseModel
{
    private string name;
    private Func<GeoCoordinate, LocationCollection> polygonFunc;

    public string Name
    {
        get
        {
            return this.name;
        }
        set
        {
            this.SetPropertyAndNotify(ref this.name, value, () => this.Name);
        }
    }

    public Func<GeoCoordinate, LocationCollection> PolygonFunc
    {
        get
        {
            return this.polygonFunc;
        }

        set
        {
            this.SetPropertyAndNotify(ref this.polygonFunc, value, () => this.PolygonFunc);
        }
    }
}

Instances of this Model are populated in the ViewModel constructor using a set of static methods on a helper class. This helper class is where the smarts for calculating polygons is contained. As you can see from the code below I've created several different shapes in different sizes.

public MapViewModel(ILog log)
{
    this.log = log;

    this.polygon = new LocationCollection();
    this.Centre = new GeoCoordinate(51.561811605968394, -0.0883626937866211);
    this.Zoom = 15;
            
    this.shapes = new ObservableCollection<Shape>
    {
        new Shape { Name = "No Shape", PolygonFunc = centre => new LocationCollection()},
        new Shape { Name = "Square (50 m)", PolygonFunc = MapFuncs.Square(0.050) },
        new Shape { Name = "Square (250 m)", PolygonFunc = MapFuncs.Square(0.250) },
        new Shape { Name = "Square (500 m)", PolygonFunc = MapFuncs.Square(0.500) },
        new Shape { Name = "Circle (50 m)", PolygonFunc = MapFuncs.Circle(0.050) },
        new Shape { Name = "Circle (250 m)", PolygonFunc = MapFuncs.Circle(0.250) },
        new Shape { Name = "Circle (500 m)", PolygonFunc = MapFuncs.Circle(0.500) },
        new Shape { Name = "Pentangle (500 m)", PolygonFunc = MapFuncs.Pentangle(0.500) },
        new Shape { Name = "Star (5 points)", PolygonFunc = MapFuncs.Star(5, 0.500) },
        new Shape { Name = "Star (6 points)", PolygonFunc = MapFuncs.Star(6, 0.500) },
        new Shape { Name = "Star (7 points)", PolygonFunc = MapFuncs.Star(7, 0.500) },
        new Shape { Name = "Star (8 points)", PolygonFunc = MapFuncs.Star(8, 0.500) },
        new Shape { Name = "Star (9 points)", PolygonFunc = MapFuncs.Star(9, 0.500) },
        new Shape { Name = "Star (10 points)", PolygonFunc = MapFuncs.Star(10, 0.500) },
        new Shape { Name = "Polygon (4 sides)", PolygonFunc = MapFuncs.Polygon(4, 0.500) },
        new Shape { Name = "Polygon (5 sides)", PolygonFunc = MapFuncs.Polygon(5, 0.500) },
        new Shape { Name = "Polygon (5 sides, offset)", PolygonFunc = MapFuncs.Polygon(5, 0.500, 36) },
        new Shape { Name = "Polygon (6 sides)", PolygonFunc = MapFuncs.Polygon(6, 0.500) },
        new Shape { Name = "Polygon (7 sides)", PolygonFunc = MapFuncs.Polygon(7, 0.500) },
        new Shape { Name = "Polygon (8 sides)", PolygonFunc = MapFuncs.Polygon(8, 0.500) },
        new Shape { Name = "Polygon (9 sides)", PolygonFunc = MapFuncs.Polygon(9, 0.500) },
        new Shape { Name = "Polygon (10 sides)", PolygonFunc = MapFuncs.Polygon(10, 0.500) }
    };

    this.SelectedShape = this.shapes.First();
}

This collection of Shapes is then bound to the View via the Shapes property on the ViewModel. The View uses this as part of ListPicker to allow the user to select the current Shape. When a Shape is selected the ViewModel raises a property notify changed event to indicate the currently selected Shape should be re-drawn. The important part is the static class MapFuncs, this is where the smarts are. It uses the Haversine formula to calculate the polygons. The Haversine method is shown below along with a couple of hepler methods for converting degrees to and from radians. The Haversine formula calculates the geo-location given a start geo-location, distance and bearing on a sphere, the sphere in case is the Earth:

private static GeoCoordinate CalculateUsingHaversine(GeoCoordinate startLocation, double distance, double bearing)
{
    var lat1 = DegreeToRadian(startLocation.Latitude);
    var long1 = DegreeToRadian(startLocation.Longitude);

    var bar1 = DegreeToRadian(bearing);
    var angularDistance = distance / EarthRadius;

    var lat2 = Math.Asin(Math.Sin(lat1) * Math.Cos(angularDistance) + Math.Cos(lat1) * Math.Sin(angularDistance) * Math.Cos(bar1));

    var lon2 = long1 + Math.Atan2(Math.Sin(bar1) * Math.Sin(angularDistance) * Math.Cos(lat1),
                                    Math.Cos(angularDistance) - Math.Sin(lat1) * Math.Sin(lat2));


    var destinationLocation = new GeoCoordinate(RadianToDegree(lat2), RadianToDegree(lon2));

    return destinationLocation;
}

private static double DegreeToRadian(double angle)
{
    return Math.PI * angle / 180.0;
}

private static double RadianToDegree(double angle)
{
    return angle * (180.0 / Math.PI);
}

As I said earlier, the formula is then used to calculate the geo-locations which describe the required polygon (shape).

Method for creating polygon describing a Circle:

public static Func<GeoCoordinate, LocationCollection> Circle(double diameter)
{
    var radius = diameter / 2;
    Func<GeoCoordinate, LocationCollection> func = location =>
    {
        var locations = new LocationCollection();

        // Calculate the the location for each degree of a circle...
        for (var i = 0; i < 360; i++)
        {
            locations.Add(MapFuncs.CalculateUsingHaversine(location, radius, i));
        }

        return locations;
    };

    return func;
}


Method for creating polygon describing a Square:

public static Func<GeoCoordinate, LocationCollection> Square(double length)
{
    Func<GeoCoordinate, LocationCollection> func = location =>
    {
        var locations = new LocationCollection();

        // Calculate the mid points of the square...
        var halfLength = length / 2;
        var north = CalculateUsingHaversine(location, halfLength, 0);
        var south = CalculateUsingHaversine(location, halfLength, 180);
        var east = CalculateUsingHaversine(location, halfLength, 90);
        var west = CalculateUsingHaversine(location, halfLength, 270);

        // Use the mid points to calculate the corners of the square...
        locations.Add(new GeoCoordinate(north.Latitude, west.Longitude));
        locations.Add(new GeoCoordinate(north.Latitude, east.Longitude));
        locations.Add(new GeoCoordinate(south.Latitude, east.Longitude));
        locations.Add(new GeoCoordinate(south.Latitude, west.Longitude));

        return locations;
    };

    return func;
}


Method for creating any polygon, e.g. hexagon, heptagon, octagon etc:

public static Func<GeoCoordinate, LocationCollection> Polygon(int sides, double diameter, double startAngle)
{
    Func<GeoCoordinate, LocationCollection> func = location =>
    {
        var locations = new LocationCollection();

        var radius = diameter / 2;
        var angle = 360.00 / sides;

        for (var i = 0; i < sides; i++)
        {
            var aggregatedAngle = (i*angle) + startAngle;
            locations.Add(CalculateUsingHaversine(location, radius, aggregatedAngle));
        }

        return locations;
    };

    return func;
}



The following 2 are the most interesting, they are also the most complicated - Star & Pentangle (pentagram). The method for creating a Star polygon allows the number of points to be defined as well as the diameter (size). The Pentangle is a special case of a Star, it gives you a more natural looking star image. Wikipedia describes a pentagram as 'the shape of a five-pointed star drawn with five straight strokes.'

The difference between two is essential the ratio between the inner and outer points of the star - I'm sure there is a technical term for this but I'm not aware of the name. For the following Star polygons I've a ratio of 2/3 and for the Pentangle I've used 8/17 - this give the distinctive Pentangle shape:



Method for creating a Star polygon:

public static Func<GeoCoordinate, LocationCollection> Star(int sides, double diameter)
{
    return Star(sides, diameter, 0);
}

public static Func<GeoCoordinate, LocationCollection> Star(int sides, double diameter, double startAngle)
{
    return Star(sides, diameter, startAngle, ((double)2)/3);
}

private static Func<GeoCoordinate, LocationCollection> Star(int sides, double diameter, double startAngle, double ratio)
{
    Func<GeoCoordinate, LocationCollection> func = location =>
    {
        var locations = new LocationCollection();

        var outerPoints = new LocationCollection();

        // Calculate the outer points, these lie on the circumference of the circle
        // described by the diameter...
        var radius = diameter / 2;
        var angle = 360.00 / sides;
        for (var i = 0; i < sides; i++)
        {
            var aggregatedAngle = (i * angle) + startAngle;
            outerPoints.Add(CalculateUsingHaversine(location, radius, aggregatedAngle));
        }

        // Distance between 2 outer points...
        var distance = (outerPoints[0].GetDistanceTo(outerPoints[1])) / 1000;
        var side = Math.Sqrt((radius * radius) - ((distance / 2) * (distance / 2)));

        // Calculate the inner points and combine with outer points...
        var halfAngle = angle / 2;
        for (var i = 0; i < sides; i++)
        {
            var aggregatedAngle = (i * angle) + (startAngle + halfAngle);
            var point = CalculateUsingHaversine(location, (side  * ratio), aggregatedAngle);

            locations.Add(outerPoints[i]);
            locations.Add(point);
        }

        return locations;
    };

    return func;
}



Method for creating a Pentangle polygon:

public static Func<GeoCoordinate, LocationCollection> Pentangle(double diameter)
{
    return Star(5, diameter, 0, ((double)8)/17);
}



I included a couple of memory counters at the bottom of the demo app to observe the memory consumption when rendering polygons onto the map control. After showing 10 polygons on the map and then removing  the polygon the following memory usage was observed. I was a little surprised by the increase of over 14 Mb in peak memory usage. I could see there being issues when rendering multiple polygons on the map control.


Removing the polygon from the map control was also not as straight forward as expected.  I had to use the same pattern as described in this post. Clearing the contents of the geo-locations collection did not work, I had to clear it then add a single value back into the collection. I ended up with the following functionality:

public LocationCollection Polygon
{
    get
    {
        this.polygon.Clear();
        this.BuildPolygon().ForEach(this.polygon.Add);

        // If the 'no shape' is selected we need to force the polygon to be removed
        // this is done by adding a point, in this case the centre location.
        if (this.polygon.Count() == 0)
        {
            this.polygon.Add(this.centre);
        }

        return this.polygon;
    }
}

The code makes use of the WP7Contrib for the base class for the ViewModel & Model classes and is referenced as an NuGet packages. The code for this demo app is available on SkyDrive.



Comments

  1. Hi
    I'm work on wp8 and by this solution my circle is oval shape.
    Do you know about this problem?
    Thank you

    ReplyDelete

Post a Comment

Popular posts from this blog

WPF tips & tricks: Dispatcher thread performance

Not blogged for an age, and I received an email last week which provoked me back to life. It was a job spec for a WPF contract where they want help sorting out the performance of their app especially around grids and tabular data. I thought I'd shared some tips & tricks I've picked up along the way, these aren't probably going to solve any issues you might be having directly, but they might point you in the right direction when trying to find and resolve performance issues with a WPF app. First off, performance is something you shouldn't try and improve without evidence, and this means having evidence proving you've improved the performance - before & after metrics for example. Without this you're basically pissing into the wind, which can be fun from a developer point of view but bad for a project :) So, what do I mean by ' Dispatcher thread performance '? The 'dispatcher thread' or the 'UI thread' is probably the most

Showing a message box from a ViewModel in MVVM

I was doing a code review with a client last week for a WPF app using MVVM and they asked ' How can I show a message from the ViewModel? '. What follows is how I would (and have) solved the problem in the past. When I hear the words ' show a message... ' I instantly think you mean show a transient modal message box that requires the user input before continuing ' with something else ' - once the user has interacted with the message box it will disappear. The following solution only applies to this scenario. The first solution is the easiest but is very wrong from a separation perspective. It violates the ideas behind the Model-View-Controller pattern because it places View concerns inside the ViewModel - the ViewModel now knows about the type of the View and specifically it knows how to show a message box window: The second approach addresses this concern by introducing the idea of messaging\events between the ViewModel and the View. In the example below

Implementing a busy indicator using a visual overlay in MVVM

This is a technique we use at work to lock the UI whilst some long running process is happening - preventing the user clicking on stuff whilst it's retrieving or rendering data. Now we could have done this by launching a child dialog window but that feels rather out of date and clumsy, we wanted a more modern pattern similar to the way <div> overlays are done on the web. Imagine we have the following simple WPF app and when 'Click' is pressed a busy waiting overlay is shown for the duration entered into the text box. What I'm interested in here is not the actual UI element of the busy indicator but how I go about getting this to show & hide from when using MVVM. The actual UI elements are the standard Busy Indicator coming from the WPF Toolkit : The XAML behind this window is very simple, the important part is the ViewHost. As you can see the ViewHost uses a ContentPresenter element which is bound to the view model, IMainViewModel, it contains 3 child v