HTML5 Canvas Pushpins in JavaScript

One of the more interesting features of HTML5 is the canvas elements. The canvas provides us with a feature rich, low level 2D rendering panel and is supported by all the major web browsers.

In this blog post, we are going to create a Bing Maps module that wraps the standard pushpin class to create a new HTML5 Canvas pushpin class. There are a lot of cool and interesting things we can do with the canvas that would require a lot more work using traditional HTML and JavaScript. By using this module, tasks such as rotating a pushpin or programmatically changing the color can be easily achieved. In this post I will use the Bing Maps Web control, but all of the code can be easily reused with the Bing Maps for Windows Store Apps JavaScript control as well.

Creating the Canvas Pushpin module

In Bing Maps, pushpins have the ability to render custom HTML content. We can take advantage of this by passing in an HTML5 as the custom HTML5 content into the pushpin options. Since we cannot access this canvas until it has been rendered, we will want to create a CanvasLayer class which wraps the EntityCollection class and fires and event with an entity is added to render the data on our canvas. We can then create a CanvasPushpin class that takes two parameters: a location to display the canvas on the map and a callback function that will receive a reference to the pushpin and to the context of the HTML5 canvas. After this, we will be able to use the canvas context to draw our data. Take the following code for the module and copy it into a new file called CanvasPushpinModule.js and store it in a folder called scripts.

/***************************************************************
* Canvas Pushpin Module
* 
* This module creates two classes; CanvasLayer and CanvasPushpin 
* The CanvasLayer will render a CanvasPushpin when it is added 
* to the layer. 
*
* The CanvasPushpin creates a custom HTML pushpin that contains 
* an HTML5 canvas. This class takes in two properties; a location 
* and a callback function that renders the HTML5 canvas.
****************************************************************/
var CanvasLayer, CanvasPushpin;

(function () {
var canvasIdNumber = 0;

function generateUniqueID() {
var canvasID = 'canvasElm' + canvasIdNumber;
        canvasIdNumber++;

if (window[canvasID]) {
return generateUniqueID();
        }

return canvasID;
    }

function getCanvas(canvasID) {
var c = document.getElementById(canvasID);

if (c) {
            c = c.getContext("2d");
        }

return c;
    }

//The canvas layer will render a CanvasPushpin when it is added to the layer. 
    CanvasLayer = function () {
var canvasLayer = new Microsoft.Maps.EntityCollection();
        Microsoft.Maps.Events.addHandler(canvasLayer, 'entityadded', function (e) {
if (e.entity._canvasID) {
                e.entity._renderCanvas();
            }
        });
return canvasLayer;
    };

    CanvasPushpin = function (location, renderCallback) {
var canvasID = generateUniqueID();

var pinOptions = {
            htmlContent: '<canvas id="' + canvasID + '"></canvas>'
        };

var pin = new Microsoft.Maps.Pushpin(location, pinOptions);

        pin._canvasID = canvasID;

        pin._renderCanvas = function () {
            renderCallback(pin, getCanvas(pin._canvasID));
        };

return pin;
    };
})();

// Call the Module Loaded method
Microsoft.Maps.moduleLoaded('CanvasPushpinModule');

To implement the module, we will need to load the module. Once it is loaded, we will want to create a CanvasLayer entity collection which we will add our canvas pushpins. Once this layer is created, we can create our CanvasPushpin objects and add them to the layer. We will use the following image and draw it onto the canvas.

Green Pin

green_pin.png

When we put this all together we end up with the following code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>

      <script type="text/javascript">
var map, canvasLayer;

function GetMap() {
// Initialize the map
              map = new Microsoft.Maps.Map(document.getElementById("myMap"),
              {
                  credentials: "YOUR_BING_MAPS_KEY"
              });

//Register and load the Canvas Pushpin Module
              Microsoft.Maps.registerModule("CanvasPushpinModule", "scripts/CanvasPushpinModule.js");
              Microsoft.Maps.loadModule("CanvasPushpinModule", {
                  callback: function () {
//Create Canvas Entity Collection
                      canvasLayer = new CanvasLayer();
                      map.entities.push(canvasLayer);

//Create the canvas pushpins
                      createCanvasPins();
              }});
          }

function createCanvasPins() {
var pin, img;

for (var i = 0; i < 100; i++) {

//Create a canvas pushpin at a random location
                  pin = new CanvasPushpin(new Microsoft.Maps.Location(Math.random() * 180 - 90, Math.random() * 360 - 180), function (pin, context) {
                      img = new Image();
                      img.onload = function () {
if (context) {
                              context.width = img.width;
                              context.height = img.height;
                              context.drawImage(img, 0, 0);
                          }
                      };
                      img.src = 'images/green_pin.png';
                  });

//Add the pushpin to the Canvas Entity Collection
                  canvasLayer.push(pin);
              }
          }
      </script>
   </head>
   <body onload="GetMap();">
        <div id='myMap' style="position:relative;width:800px;height:600px;"></div>
   </body>
</html>

By using the above code, we will end up with something like this:

Canvas Basic

A more colorful pushpin

Using the canvas to render an image isn’t terribly exciting, especially when we could do the same thing by setting the icon property of the pushpin to point to the image and accomplish the same thing. One common question I’ve seen on the Bing Maps forums over the years is “How do I change the color of the pushpin?”. In the past the answer was always the same; create a new image that is a different color. In Bing Maps Silverlight, WPF and Native Windows Store controls we can change the color programmatically by simply setting the background color. When using this canvas pushpin module we can actually do the same thing. The first thing we will need is a base pushpin that has the color region removed and made transparent. Below is the image we will use in this example:

 

Transparent Pin

transparent_pin.png

To get this to work nicely, we will want to provide each pin with a color in which we want it to render as. To do this, we can add a metadata property to each pushpin and store our color information in there. The color information itself is a string representation of a color that the HTML5 canvas can understand. This could be a HEX or RGB color. In this example, we will render random colors for each pushpin. When we go to render the pushpin we will want to draw a circle that is the specified color and then overlay our pushpin template overtop. Below is a modified version of the createCanvasPins method that shows how to do this.

function createCanvasPins() {
var pin, img;

for (var i = 0; i < 100; i++) {

//Create a canvas pushpin at a random location
        pin = new CanvasPushpin(new Microsoft.Maps.Location(Math.random() * 180 - 90, Math.random() * 360 - 180), function (pin, context) {
            img = new Image();
            img.onload = function () {
if (context) {
//Set the dimensions of the canvas
                    context.width = img.width;
                    context.height = img.height;

//Draw a colored circle behind the pin
                    context.beginPath();
                    context.arc(13, 13, 11, 0, 2 * Math.PI, false);
                    context.fillStyle = pin.Metadata.color;
                    context.fill();

//Draw the pushpin icon
                    context.drawImage(img, 0, 0);
                }
            };

            img.src = 'images/transparent_pin.png';
        });

//Give the pushpin a random color
        pin.Metadata = {
            color: generateRandomColor()
        };

//Add the pushpin to the Canvas Entity Collection
        canvasLayer.push(pin);
    }
}

function generateRandomColor() {
return 'rgb(' + Math.round(Math.random() * 255) + ',' + Math.round(Math.random() * 255) + ',' + Math.round(Math.random() * 255) + ')';
}


Using the above code we will end up with something like this:

Canvas Colored Pins

Rotating a pushpin

Sometimes it is useful to be able to rotate your pushpin. One of the most common scenarios I have come across for this is to be able to draw arrows on the map that point in a specific direction. Using traditional HTML and JavaScript we would do this in one of two ways. The first method would be to create a bunch of images of arrows pointing in different directions and then choose which ever one is closest to the direction we want. The second method is a bit of a CSS hack involving the use of borders which I talked about in a past blog post. When using the HTML5 canvas, we can easily rotate the canvas by translating the canvas to the point we want to rotate about, then rotating the canvas, and translating the canvas back to the original position. One thing we need to take into consideration is that in most cases the dimension of the canvas will need to change as we rotate an image. Think about rotating a rectangle. When rotated by a few degrees, the area the rectangle requires to fit inside the canvas will be larger. When rotating objects on a map, it is useful to know that it is common practice for North to represent a heading of 0 degrees, East to be 90 degrees, South to be 180 degrees and West to be 270 degrees. In this example, we will take the following arrow image and provide and add metadata to the pushpin where we will specify the heading for the arrow to point. And to make things easy, we will make our arrow point up which aligns with the north direction and a heading of 0 degrees.

Red Arrow

redArrow.png

When we put this all together, we end up with the following code:

function createCanvasPins() {
var pin, img;

for (var i = 0; i < 100; i++) {

//Create a canvas pushpin at a random location
        pin = new CanvasPushpin(new Microsoft.Maps.Location(Math.random() * 180 - 90, Math.random() * 360 - 180), function (pin, context) {
            img = new Image();
            img.onload = function () {
if (context) {
//Calculate the new dimensions of the the canvas after the image is rotated
var dx = Math.abs(Math.cos(pin.Metadata.heading * Math.PI / 180));
var dy = Math.abs(Math.sin(pin.Metadata.heading * Math.PI / 180));                              
var width = Math.round(img.width * dx + img.height * dy);
var height = Math.round(img.width * dy + img.height * dx);

//Set the dimensions of the canvas
                    context.width = width;
                    context.height = height;

//Offset the canvas such that we will rotate around the center of our arrow
                    context.translate(width * 0.5, height * 0.5);

//Rotate the canvas by the desired heading
                    context.rotate(pin.Metadata.heading * Math.PI / 180);

//Return the canvas offset back to it's original position
                    context.translate(-img.width * 0.5, –img.height * 0.5);

//Draw the arrow image
                    context.drawImage(img, 0, 0);
                }
            };
            img.src = 'images/redArrow.png';
        });

//Give the pushpin a random heading
        pin.Metadata = {
            heading: Math.random() * 360
        };

        canvasLayer.push(pin);
    }
}

Using the above code we will end up with something like this:

Canvas Arrows

Data Driven pushpins

Bing Maps is often used in business intelligence applications. One particular type of business intelligence data that users like to overlay on the map are pie charts. The good news is that creating pie charts using a canvas is fairly easily. Each portion of a pie chart will be a percentage of the total of all the data in the pie chart. Let’s simply make use of the arc drawing functionality available within the canvas to do this.

function createCanvasPins() {
var pin;

//Define a color for each type of data
var colorMap = ['red', 'blue', 'green', 'yellow'];

for (var i = 0; i < 25; i++) {

//Create a canvas pushpin at a random location
        pin = new CanvasPushpin(new Microsoft.Maps.Location(Math.random() * 180 - 90, Math.random() * 360 - 180), function (pin, context) {
//Calculate the number of slices each pie we can support
var max = Math.min(pin.Metadata.data.length, colorMap.length);

//Calculate the total of the data in the pie chart
var total = 0;
for (var i = 0; i < max; i++) {
                total += pin.Metadata.data[i];
            }

//Draw the pie chart
            createPieChart(context, total, pin.Metadata.data, colorMap);
        });

//Give the pushpin some data
        pin.Metadata = {
            data: generateRandomData()
        };

//Add the pushpin to the Canvas Entity Collection
        canvasLayer.push(pin);
    }
}

function createPieChart(context, total, data, colorMap) {
var radius = 12.5,
        center = { x: 12.5, y: 12.5 },
        lastPosition = 0;

    context.width = 25;
    context.height = 25;

for (var i = 0; i < data.length; i++) {
        context.fillStyle = colorMap[i];
        context.beginPath();
        context.moveTo(center.x, center.y);
        context.arc(center.x, center.y, radius, lastPosition, lastPosition + (Math.PI * 2 * (data[i] / total)), false);
        context.lineTo(center.x, center.y);
        context.fill();
        lastPosition += Math.PI * 2 * (data[i] / total);
    }
}

function generateRandomData() {
return [Math.random() * 100, Math.random() * 100, Math.random() * 100, Math.random() * 100];
}

Using the above code we will end up with something like this:

Canvas Pie Chart

If you wanted to take this module even further, you could create pushpins that are animated, or even simulate a 3D object.

– Ricky Brundritt, EMEA Bing Maps Technology Solution Professional