Skip to main content

How many pins can Bing Maps handle in a WP7 app - part 1


part2 - http://awkwardcoder.blogspot.com/2011/10/how-many-pins-can-bing-maps-handle-in.html
part3 - http://awkwardcoder.blogspot.com/2011/11/how-many-pins-can-bing-maps-handle-in.html

Rich & I've been working on a WP7 app recently which annotates the Bing Maps control with push pins. These pins represent a point of interest - a reported crime. We've been using the UK crime stats which are provided free of charge, all you have to do is register for an account here. They expose the data using a RESTful service which exposes the data as JSON over HTTP GET requests. It was relatively easy to build a service layer to consume and map this data into a set of model classes for binding to a UI. The RESTful service has multiple endpoints, some are designed to build complex queries spanning multiple HTTP requests and others are single requests based around your current location. The later is what I'll be using for this post.

The API I'm using is the 'street-level crime' endpoint, detailed here. It provides all crimes that have occurred in a 1 mile radius of a geo-location and does not guarantee accuracy of crime locations. An example URL is shown below:


The data returned is truly dynamic - I have no idea how many results will be returned for a geo-location, the amount will vary over time as well as geo-location.

My first task was to get this working and returning data, I wasn't binding the data to the UI at this stage only making sure data was being returned. Shown below is the high level code I used. 

The code snippet is the constructor for the page class and an event handler for the map control. The highlighted area shows how I get the data, when ever the view has finished changing the event handler is called and we then request the crime data. You can see from the second highlighted area the service request, it uses Rx (reactive extensions) to handle the asynchronous nature of making a request over the internet. The result count (the crime count) is written out to a log. The log happens to write out to the debug output and is shown below along with the app.



As you can see the crime count for the 1 mile radius around the geo-location (51.556555, -0.0768184) is 1695!


Baseline memory

The UI for this version consists of a Bing Maps control with a couple of text boxes at the bottom showing the peak and current memory. These are updated regularly (< 250 ms) using a DispatcherTimer class and querying the device extended properties for current and peak memory usage.

The screenshot shown on the left has values of around 21.12 Mb - the baseline the map control needs to show a map centred on the above location without panning\zooming the map control.
































The next stage was to overlay the push pins and see what happens. I started with overlaying all the returned crime data in one go - iterating over the returned data and adding to the MapItemsControl class. This class was defined in the XAML as shown below, an item template was then applied to the class to get the pin to render. We use a converter to parse the crime category and give it a different colour.

All 1695 pins

You'll notice straight away problems with this approach.

Firstly I can't use the map whilst its adding the pins, the app freezes and does not response to any input. IT takes over 20 seconds from receiving the response from the RESTful service to the pins being shown and the app being usable again. This is a symptom of trying to show to much information to the user and all it will do is confuse and give the perception of a bad user experience. This is in part due to the location where the search was done and the resolution of the map control (zoom = 16).

Shown below is the debug output and the highlighted lines show the time taken to add the pins to the map control - over 20 seconds.



Secondly the memory usage has jumped a whopping 53 Mb! Its well on its way to the 90 Mb limit. Imagine if this page was part of a large app, I can easily see this causing the app to surpass the 90 Mb limit. 

This is due to the fact we are only observing a fraction of the pins added to the map control - the view port is only showing a subset of the available data. I could see them all by zooming out but the following graphics represents the problem perfectly - the pins are being added to the map control even though they are not currently being displayed.



Thirdly every time you scroll the map control it will make another request for data. This in its self is not the problem, its the frequency of the firing of the ViewChangeEnd event. Every time I lift my finger off the screen after scrolling the event would fire, so if I want to scroll more than once the event would fire multiple times. The debug output shows multiple requests being made simultaneously and this is not what we want:




A side effect of multiple requests to the RESTful service is the memory usage for the device rockets! Its easy to get the value above 200 Mb and eventually you'll get an OutOfMemory exception.




And Fourthly when adding pins to the MapItemsControl class it only checks if a pins exists using reference equality, therefore if you have to separate instances for the same pin then it will be added twice to the map control. I suspect this is causing the majority of the memory allocation shown above.


Tackling the third & fourth problems first I should be able to reduce the memory usage and provide good code for future versions of the code base. 

The first task was to deal with multiple requests happening at the same time. This is done by cancelling any currently executing request and then starting the new request. This was easy to do because we are using Rx. All we have to do is dispose of the current subscriber ( 'currentSubscriber') and initiate a new request.

Shown below is the modified code, it has the null check and explicit dispose of the 'currentSubscriber' variable before the assigment of the new subscriber.

private void HandleViewChangeEnd(object sender, MapEventArgs mapEventArgs)
{
    var criterion = new StreetLevelCrimeCriterion { Latitude = this.map.Center.Latitude, Longitude = this.map.Center.Longitude };
            
    this.log.Write(string.Format("Started - ({0}, {1})", criterion.Latitude, criterion.Longitude));

    if (this.crimeSubscriber != null)
    {
        this.crimeSubscriber.Dispose();
    }

    this.crimeSubscriber = this.crimeService.SearchCrimeRelatedStreetLevelCrime(criterion)
        .ObserveOnDispatcher()
        .Subscribe(result =>
                        {
                            this.log.Write(string.Format("Crime count = {0}, ({1}, {2})", result.Crimes.Count, criterion.Latitude, criterion.Longitude));

                            foreach (var crime in result.Crimes)
                            {
                                this.MapPins.Items.Add(crime);
                            }
                        },
                    exception => this.log.Write(string.Format("Exception, message - '{0}'", exception.Message)),
                    () => this.log.Write(string.Format("Completed - ({0}, {1})", criterion.Latitude, criterion.Longitude)));
}

Now when the code executes I see something similar to below. It shows 5 requests being made simultaneously but only 1 set of results being processed and the results being process are the result from the last request. You will also notice the highlighted screenshot of the device emulator - the memory consumption is considerably less then 200 Mb, still not perfect but better.




Next to address is multiple pins for the same location. As I said it appears the MapItemsControl class uses reference equality to check for multiple pins and since there are multiple instances for the same location all I need to do is filter any existing before adding. This is done with a LINQ query:

foreach (var crime in result.Crimes.Where(crime => !this.MapPins.Items.Cast<StreetLevelCrime>().Any(c => c.Id == crime.Id)))
{
    this.MapPins.Items.Add(crime);
}

this.count.Text = "Pin count - " + this.MapPins.Items.Count;

The screenshot below shows 3 requests being made to the RESTful service. The aggregated pin count would be 5172 (1695 + 1809 + 1668) without the above code but as you can see from the highlighted emulator device the actual pin count is 2245.

The memory usage is still not acceptable, we are not seeing memory usage around the 200 Mb any more but 90Mb is still too much.




I can now address the first and second problems - the time taken to add pins and the amount of memory consumed. To tackle these issues we are going to have to reduce the number of pins on the map control.

The simplest way to do this is to only add the pins required to be shown to the collection of the MapItemsControl  class. This is achieved by taking the current viewable bounding rectangle of the map control and only adding pins which fall inside this rectangle. This does require the removing of any existing pins added to the collection which do not fall inside the viewable bounding rectangle. The bounding rectangle is expressed as a LocationRect class which is a set of geo-locations - north, west, east, south. All I have to do is calculate if a pins fall inside this bounding rectangle. To do this I have updated the LINQ query:

var rectangle = this.map.BoundingRectangle;

this.MapPins.Items.Clear();
foreach (var crime in result.Crimes.Where(crime => (crime.Location.Latitude <= rectangle.North) &&
                                        (crime.Location.Latitude >= rectangle.South) &&
                                        (crime.Location.Longitude >= rectangle.West) &&
                                        (crime.Location.Longitude <= rectangle.East)))
{
    this.MapPins.Items.Add(crime);
}
                                   
this.count.Text = "Pin count - " + this.MapPins.Items.Count;

You can now see from the screenshot below the pin count has greatly reduced to between 110 - 240 and more importantly the memory usage is at a more acceptable level. In fact the peak memory usage is now only 61.86 Mb.



This is now approaching a usable solution, but there a couple issues still affecting the UI. Firstly the updating of the map is not very fluid, in fact its rather jumpy - when the new pins are added they suddenly appear on the map without warning and there isn't any feedback to user about what is going on.

To address this I have added a progress bar to the UI. This is controlled by the ViewChangeEnd event handler - the progress bar is started before the request to the RESTful service and stopped when either request completes successfully or fails.

And secondly we 'trickle' the data to the map. We use an implementation of the  ITrickleToCollection<T> interface in the WP7Contrib. The implementation trickles data from a source collection to a destination collection based on the interval of a DispatcherTime, this is to get round the performance penalty of adding a large number of items to UI bound collection - stop it hogging the UI dispatcher thread!

The trickler interface is defined as follows:
public interface ITrickleToCollection<T>
{
    Queue<T> Source { get; }
    bool Pending { get; }
    bool IsTrickling { get; }

    void Start(int trickleDelay, IEnumerable<T> sourceCollection, IList<T> destinationCollection);
    void Stop();

    void Suspend();
    void Resume();
}

The update ViewChangeEnd event handler now looks like this, I could refactor this more into separate method but that is not the purpose of this post.
private void HandleViewChangeEnd(object sender, MapEventArgs mapEventArgs)
{
    if (this.crimeSubscriber != null)
    {
        this.crimeSubscriber.Dispose();
    }

    var criterion = new StreetLevelCrimeCriterion { Latitude = this.map.Center.Latitude, Longitude = this.map.Center.Longitude };

    // Start progress bar at top of the view...
    this.mapBusy.IsIndeterminate = true;

    // Stop any trickling of pins...
    this.trickler.Stop();
            
    this.crimeSubscriber = this.crimeService.SearchCrimeRelatedStreetLevelCrime(criterion)
        .ObserveOnDispatcher()
        .Subscribe(result =>
                        {
                            var rectangle = this.map.BoundingRectangle;

                            // All the pins to add to the map control...
                            var allPinsToAdd = result.Crimes.Where(crime => (crime.Location.Latitude <= rectangle.North) &&
                                                                (crime.Location.Latitude >= rectangle.South) &&
                                                                (crime.Location.Longitude >= rectangle.West) &&
                                                                (crime.Location.Longitude <= rectangle.East)).Distinct().ToList();

                            // All the pins already added to the map control we want to keep...
                            var alreadyAdded = allPinsToAdd.Intersect(this.mapPins.Items.Cast<StreetLevelCrime>()).ToList();

                            // The new pins to be added to the mapp control...
                            var pinsToAdd = allPinsToAdd.Except(alreadyAdded).ToList();
                                   
                            // The existing pins to be removed which aren't visible...
                            var pinsToRemove = this.mapPins.Items.Cast<StreetLevelCrime>().Except(alreadyAdded).ToList();

                            // Remove the pins...
                            pinsToRemove.ForEach(p => this.mapPins.Items.Remove(p));

                            // Trickle the pins to map control (10 ms delay)...
                            this.trickler.Start(10, pinsToAdd.Cast<object>(), this.mapPins.Items);
                        },
                    exception =>
                        {
                            // Stop the progress bar at top of the view...
                            this.mapBusy.IsIndeterminate = false;
                        },
                    () => {});
}

Shown below are some static screenshots of the above code executing, you can see the application starting, adding pins (x2) and finally when it has completed, you'l notice the progress bar is no longer visible. I've also highlighted the memory usage, it appears now we are trickling data to the map control we aren't see as high peak memory usage as well.



There are a couple of scenarios I haven't covered off - they relate to the ability of the user to understand and interpret the data when there are so many pins. This can occur when there's a lot of pins for a small area (like above) or when the user zooms out to such a level there is also to many pins to see the map. I'm not going to cover this here. Simply I would limit the number of pins that can be added to the map control. This could be via some business logic or via a simple max count value.

Back to the original question - How many pins can Bing Maps handle in a WP7 app?

I think the answer depends ;)  but in general I'm seeing a map control with a zoom level of 16 can easily handle 100 pins and at this level the responsiveness of the control is not the problem but the density of the pins. You can if you want add over a 1000 pins to a map and it will still be lower than the 90 Mb limit but this doesn't really give the rest of your app much room to manoeuvre with respect to memory.

 I've put the code up on SkyDrive, you'll need a username & password for the UK crime stats service to run the code.


Comments

  1. Great blog post! Dynamically adding and removing pins according to BoundingRectangle change is something I've never thought of.

    Having 100 pins displayed on the map control may not pose a problem in the emulator but AFAIK it certainly causes performance issues on actual devices. Bingle Maps allows you to add as many pins as you like and I'm already experiencing significant lags when the pins are over 50.

    You may consider using BitmapCache for all of those pins so that Silverlight doesn't redraw them on each LayoutUpdate event.

    ReplyDelete
  2. I did all tests on a couple of devices (Samsung Omina 7 & LG Mule).

    BitmapCache - thanks for reminding me ;)

    ReplyDelete
  3. Nice post ... good info on dealing with map pins on WP7. If you have an iOS device available, and can access the US app store, check out this app: http://itunes.apple.com/us/app/id454060850?mt=8&ign-mpt=uo%3D4.

    I helped build this app for a charity. I'm a .Net/WP7 dev though so I just worked on the service for getting the data, not the actual iOS code. Still, it's interesting to see how the map pins are handled, even though I can't tell you exactly how it was done. :-)

    It would be interesting to see if this could be done on WP7. Basically, the idea is that pins that are really close together based on the current zoom level are grouped into one pin (with a tooltip if you tap the pin telling you how many locations are there). As you zoom in towards one of those grouped pins, the individual pins fly out to show you the individual locations.

    It's a really cool effect, and makes this type of map app easier to use.

    ReplyDelete
  4. Thanks for link I'll have a look.

    As for the grouping of the pins, it's an interesting idea and worthy of a follow up post with more perf and UI tweaks.

    ReplyDelete
  5. Great post and very timely for some similar work I'm involved in. Thanks!

    ReplyDelete
  6. I must say your article is astonishing. The clarity of it is simply amazing and no doubt you are a proficient in this field.

    ReplyDelete

Post a Comment

Popular posts from this blog

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

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

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