• Ryan Elkins
  • Jul 24,2008
  • In: Code

Heatmaps in WPF using C#: Part 3 Mapping

OK, so if you followed the previous two tutorials, you should have a decent heat map going. Now, that will work just fine if you are using it by itself, but what if the data behind it is geographic and you want to layer your heatmap over a map of say, the US? If that’s the case then we still have a bit of work to do.

You’ll need to have a heatmap created at this point. The previous two parts of this series should be enough to get that going:

Before we get started on overlaying the heatmap we need to understand some stuff about maps and the current way they are developed for us to use. The best reference I found was an MSDN article entitled Virtual Earth Tile System. I would recommend that to anyone that is really serious about this.

The important points are such:

First, mapping longitude and latitude over a map in pixel coordinates is harder than you think. In this example I am going to be talking about laying your heatmap over a static map, not making a dynamic version in Virtual Earth or Google Maps but the concept is the same. for the overlay to work properly you’ll have to get a pixel coordinate of the longitude and latitude.

Here is the C# version I am using of the algorithm mentioned in the article.

private Point ConvertToPoint(Point latLon)
{
    double sinLatitude = Math.Sin(latLon.X * Math.PI / 180);
    double pixelX = ((latLon.Y + 180) / 360) *
        256 * Math.Pow(2, 6);
    double pixelY = (0.5 - Math.Log((1 + sinLatitude) /
        (1 - sinLatitude)) / (4 * Math.PI)) *
        256 * Math.Pow(2, 6);
    return new Point(pixelX, pixelY);
}

The one part of this that will vary is the 6 in the Math.Pow(2,6). This will be based on what map zoom level you are using for your back image. I had a hard time determining exactly what level I was at, so I just entered two known points and made sure the difference between them was right. I say that because this equation, with the proper inputs, will give you the pixel coordinates of all your points relative to each other. They won’t be relative to your map yet.

Speaking of which - we need a map.

I created mine by simply taking screenshots of a Virtual Earth map and stitching the individual pieces together in Photoshop. One goal with the personal project that started this whole thing was to display it on a widescreen, 1080p LCD TV. 1080p is 1920×1080 so I needed a map that big. Now unfortunately, between the various zoom levels, there isn’t a good level where you can fit the entire earth onto one image at 1920×1080. It’s either too big or too small.

The solution I came up with was to make a “too big” version and shrink it down. This means that all my pixel coordinates would have to be multiplied by this ratio as well. One that was done I had to align my heatmap and my map. I did this by creating points at Seattle and Miami and then adding or subtracting to shift the image into the correct place. I had a real hard time because while I could get one point, the other was always off.

It took me a while to realize it but the problem wasn’t my math, it was XAML. The image that held the map had been set to “stretch” so it was distorted and that’s why everything looked “off”. Here is the XAML I am currently using in my version.

    <Window x:Class="Mapper.HeatMapper"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="HeatMap" Height="1100" Width="1940">
   <Grid>
        <Image Margin="0,0,0,0" Name="map" Height="1080" Width="1920">
            <Image.Source>
                <BitmapImage DecodePixelWidth="1920"
                    UriSource="C:\Code\randomimages\mapus1920.png" />
            </Image.Source>
        </Image>
        <Image Margin="0,0,0,0" Name="heatImage" Height="1080"
            Width="1920" />
    </Grid>
</Window>

If you were following everything above you’ll also notice that I had to make some adjustments to my pixel conversion method. Here is how I altered it to work with the image that I am currently using.

private Point ConvertToPoint(Point latLon)
{
    double multipleOffset = (1080.0 / 1613);
    double sinLatitude = Math.Sin(latLon.X * Math.PI / 180);
    double pixelX = ((latLon.Y + 180) / 360) *
        256 * Math.Pow(2, 6) * multipleOffset - 1658;
    double pixelY = (0.5 - Math.Log((1 + sinLatitude) /
        (1 - sinLatitude)) / (4 * Math.PI)) *
        256 * Math.Pow(2, 6) * multipleOffset - 3678;
    return new Point(pixelX, pixelY);
}

The multiplier offset was determined by taking the new width (1080) and dividing by the original width of the picture. This would only work if your original picture was in the same ratio of 1920/1080, of course. The offset was determined by looking at known points on a map in Photoshop and determining what the pixel coordinates of those points are, and then stepping through the code to see what the ACTUAL coordinates where, and adjusting them accordingly.

Once you have converted your longitude and latitude to pixel coordinates, you can feed those coordinates into your methods that create the black and white mask and then off to be colorized. The images should lay on top of each other, with the heatmap on top and it should look pretty good.

Because I’m such a nice guy, I’ll even give you the 1920×1080 map of the U.S. I created (it’s about 2.6 Mb). If you try to use it, note the multiple offset I used above (1080.0/1613). the “.0″ tagged on the end of 1080 is to make sure it gives you a decimal and doesn’t do integer division (which would simply return 0).

After all of this you should have enough information to get your heatmap lined up over a static map image. The next part (if I ever get to it - not sure if it will happen or not) will be about creating image tiles so that your heatmap can work on dynamic maps (like Virtual Earth - with zooming, panning, switching between road and aerial view, etc).

Here is a sample of what the current version of my heatmap looks like.

A sample heatmap created using these tutorials.

Sample heatmap created using these tutorials.

Same as above but showing how adjustments can be made to the gradient - here we see the alpha turned down a bit more.

Same as above but showing how adjustments can be made to the gradient - here we see the alpha turned down a bit more.

If you like this blog please take a second and subscribe to my rss feed

Tags: , , , , , ,

Comments: 5 comments

All the fields that are marked with REQ must be filled

  • Ryan Abreu
    July 24th, 2008 at 10:20 am

    The only thing I would change about this implementation of the heat map is adding a bit of transparency to the heat map itself. That way, you can see the city names beneath the points.

  • Ryan Elkins
    July 24th, 2008 at 10:28 am

    There is transparency. Perhaps you mean MORE transparency. The only issue with that is that it starts to make it harder to see the heatmap itself. Obviously, it would depend somewhat on the exact implementation and what you are using the map for. I’ll try and post an example with more transparency later.

  • Ryan Abreu
    July 24th, 2008 at 10:44 am

    Overall, the heat map looks very good. I’m actually pretty impressed.

  • CHristian
    August 3rd, 2008 at 11:23 pm

    hi there,
    looks really good! :)

    could this be used on a webpage as well?
    let’s say we log every link click on a page or every mouse click on the page to be more accurate… and then generate the data from this?

    hope to hear from you,
    Christian

  • ryan
    August 4th, 2008 at 10:25 am

    You could use this to create a heatmap of just about anything. As long as you have a collection of locations the above code should work.

    The real question is how do you get accurate locations for a mouse click? Depending on how the website is set up that could be an interesting issue with people running at different resolutions. If it is fluid width or fixed width centered (like this blog) you’d have to take window size in to consideration as well to get an accurate position.

Leave a reply

Name (Req)

E-mail (Req)

URI

Message