Udostępnij za pośrednictwem


How to Create Heat Maps in Native Windows Store Apps

Heat maps, also known as density maps, are a type of overlay on a map used to represent data using different colors. Heat maps are often used to show the data hot spots on a map. The data used in these overlays usually takes one of two forms:

  • Color coded polygons or polylines - In this form, the polygons and polylines are based on some metric related to those shapes. Creating this type of heat map is fairly easy as you simply need to assign a color to the shape when adding it to the map.
  • Point based data - This form uses point-based data where the colors of the heat map are based on the density of the data points on the map. Creating these types of heat maps is a bit more complex.

If you are working in JavaScript there is a client-side heat map module available in the Bing Maps V7 Modules project, which can be used in Windows Store apps. Using this module is fairly easy and only takes a few lines of code to setup. Creating heat maps in native Windows Store apps is a bit harder to do.

In this blog post I describe how I came up with a solution for creating heat maps in native Windows Store apps and document the reusable library that I created. Full source code can be downloaded from the MSDN Code Samples here.

In Windows Store apps our main option for doing any kind of graphics generation is to use DirectX in C++ or SharpDX from C# or VB. If you have little to no experience with either of these libraries, it can a pretty painful learning curve.

After a lot of work I managed to create a reusable library using SharpDX. Unfortunately, I was not entirely satisfied with the performance. After putting this aside and revisiting it from time to time over the past year it always frustrated me that I could easily do what I was trying to accomplish using JavaScript, but not in C# or VB. Then it hit me: Why not generate the heat map using JavaScript and pass the generated image to my native code?

The idea is to create a custom control that inherits from the Grid control. We can pass a Grid control as a child of the map without setting its position, and this will cause the Grid to fill the full map area. We can then add a WebView control as a child of the custom control, and then write some JavaScript and HTML to render a heat map using the HTML5 canvas. Then, we can load this in the WebView and have it appear above the map.

There is one issue with this: The WebView control does not support transparent backgrounds. We can use an opacity, but then we end up with a whitish shadow in the empty heat map area. To solve this, we can capture the image information from the HTML5 canvas and return it as a data string to our native code. We can then add an Image control to our custom control and set the source of the Image to this data string. This approach worked well with data sets of 5,000 or less points, but often blocked the UI thread for a few seconds.

Not being content with this, I started to make some tweaks. Creating density heat maps is a two-step process:

  1. Draw each data point on the map as a semi-transparent gray scale radial gradient. This creates an alpha mask of the heat map.
  2. Map a color gradient to each pixel based on the alpha value of each pixel.

To optimize the first part, rather than drawing the radial gradients, I created an image of the required gradient in an image editor. I then scaled the image and drew it to the canvas. This worked faster than drawing the radial gradients. A second optimization was around the pixel color mapping step. I retrieved the pixels from the Canvas using the getImageData function. This creates an array that consists of 4 cells for each pixel on the canvas. In a heat map that is 1600 x 900 pixels in size there are 1,440,000 pixels. This means that during the colorization step, the code has to loop through an array that has 5,760,000 cells. It was this process that was mainly locking up the UI thread.

Doing some research I came across this excellent blog post on using web workers to improve performance of image manipulation. A web worker allows you to create background threads in JavaScript. Doing this made a big difference. The final heat map works great with data sets of 50,000 data points. The more data points there are the slower it will become, but while testing I found that even a data set of 500,000 data points had acceptable performance, however generating that much test data in the app was pretty slow.

The library I created uses the namespace Bing.Maps.HeatMaps. This library consists of a single class called HeatMapLayer and also contains the required HTML, JavaScript and images files to generate the heat map. Here is a screenshot of the type of heat map that this library generates:

The HeatMapLayer class provides a number of properties which can be used to customize how the heat map is rendered. Here is a list of the different properties available:

Name Type Description
HeatGradient GradientStopCollection A color gradient used to colorize the heat map.
Intensity double Intensity of the heat map. A value between 0 and 1.
Locations LocationCollection Collection of locations to plot on the heat map.
ParentMap Map A reference to the parent Bing Maps control.
Radius double Radius of data point in meters.
EnableHardEdge bool Gives all values the same opacity to create a hard edge on each data point. When set to false (default) the data points will use a fading opacity towards the edges.

The heat map will not render until the ParentMap property is assigned. All the properties with the exception of this one support data binding. If you decided to add a heat map to your map using XAML you will need to set the ParentMap property in your code behind. Here is an example of how to add a heat map to your map using XAML.

 <Page
    x:Class="HeatMapTestApp.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:HeatMapTestApp"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:m="using:Bing.Maps"
    xmlns:hm="using:Bing.Maps.HeatMaps”>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">       
        <m:Map Name="MyMap" Credentials="YOUR_BING_MAPS_KEY">
            <m:Map.Children>
                <hm:HeatMapLayer Name="layer" Radius="2500"/>
            </m:Map.Children>
        </m:Map>
    </Grid>
</Page>
 You can then set the ParentMap property using the following code. You also can data bind the Location property, but for simplicity in this example I have just set it from code.
C#
 using Bing.Maps;
using Microsoft.Maps.SpatialToolbox;
using Microsoft.Maps.SpatialToolbox.Bing;
using System;
using Windows.UI.Xaml.Controls;

namespace HeatMapTestApp
{
    public sealed partial class MainPage : Page
    {
        private LocationCollection locs;

        public MainPage()
        {
            this.InitializeComponent();

            this.Loaded += (s, e) =>
            {
                locs = new LocationCollection();

                //Add location data to collection…

                layer.ParentMap = MyMap;
                layer.Locations = locs;
            };
        }
    }
} 
Visual Basic
 Imports Bing.Maps

Public NotInheritable Class MainPage
    Inherits Page

    Private layer As HeatMapLayer
    Private locs As LocationCollection

    Public Sub New()
        InitializeComponent()

        AddHandler MyMap.Loaded, Sub()
                                     locs = New LocationCollection()

                                     'Add location data to collection…

                                     layer.ParentMap = MyMap
                                     layer.Locations = locs
                                 End Sub
    End Sub
End Class

Alternatively, you can create the heat map completely from code and add it as a child of the map. The following is an example of how to add a HeatMapLayer to Bing Maps using code.

C#
 using Bing.Maps;
using Microsoft.Maps.SpatialToolbox;
using Microsoft.Maps.SpatialToolbox.Bing;
using System;
using Windows.UI.Xaml.Controls;

namespace HeatMapTestApp
{
    public sealed partial class MainPage : Page
    {
        private HeatMapLayer layer;
        private LocationCollection locs;

        public MainPage()
        {
            this.InitializeComponent();

            this.Loaded += (s, e) =>
            {
                locs = new LocationCollection();

                //Add location data to collection…

                layer = new HeatMapLayer()
                {
                    ParentMap = MyMap,
                    Locations = locs,
                    Radius = 2500
                };

                MyMap.Children.Add(layer);
            };
        }
    }
} 
Visual Basic
 Imports Bing.Maps
Imports Microsoft.Maps.SpatialToolbox
Imports Microsoft.Maps.SpatialToolbox.Bing

Public NotInheritable Class MainPage
    Inherits Page

    Private layer As HeatMapLayer
    Private locs As LocationCollection

    Public Sub New()
        InitializeComponent()

        AddHandler MyMap.Loaded, Sub()
                                     locs = New LocationCollection()

                                     'Add location data to collection…

                                     layer = New HeatMapLayer()
                                     layer.ParentMap = MyMap
                                     layer.Locations = locs
                                     layer.Radius = 2500

                                     MyMap.Children.Add(layer)
                                 End Sub
    End Sub
End Class

The full source code for this library and a sample app that demonstrates how to use the different features of this library can be found here. Here is a screenshot of the test app that is included in this library.

If you are looking for some other great resources on Bing Maps for Windows Store apps, look through this blog and check out all the Bing Maps MSDN code samples.

Comments