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 V7 AJAX 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:

canvasBasic

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:

canvasColoredPins

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.

redArrow

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:

canvasArrows

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:

canvasPieChart

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

12 thoughts on “HTML5 Canvas Pushpins in JavaScript

  1. I also tried integrating your CustomInfoBox solution with HTML5 Canvas solution but still the problem remains the same. Here is what I did.

    function createCanvasPins(canvasLayer) {

    for (var i = 0; i Here is some custom HTML”;

    //Add a handler for the pushpin click event.

    Microsoft.Maps.Events.addHandler(pin, ‘click’, displayInfobox);

    //Add the pushpin to the Canvas Entity Collection

    canvasLayer.push(pin);

    }

    }

    Rest of working is same as it is in CustomInfoBox solution. This works in IE9+ but still no progress in chrome or Firefox(latest versions).

    I really don’t know where i’m going wrong. When I plot normal(without colored) pushpins everything works.

    I hope someone can help me point at right direction.

  2. Thanks for the quick reply. Your answer solved my problem.

    This is exactly what solved the problem, in case anyone else lands up with the same problem.

    //Add a handler for the pushpin click event.
    Microsoft.Maps.Events.addHandler(pin, ‘click’, displayInfobox);
    //for firefox , chrome an other browsers
    $(‘#myMap’).find(‘.MapPushpinBase’).css(‘pointer-events’, ‘auto’);
    //Add the pushpin to the Canvas Entity Collection
    canvasLayer.push(pin);

  3. Hi,
    I have implemented the canvas pushpin module solution and it works, many many thanks for your previous answer Ricky Brundritt. But sometimes[2 in 10 iterations] i get “document.getElementById(canvasID)” as “null”, though the record exists. Since i get this as “null” the pushpin is not plotted on the map. You can check the screen shot from here ” http://s22.postimg.org/53s8l030h/canvas_Problem.png “.

    Example- I have 5 records that needs to be plotted, but sometimes only 3 of them gets plotted & the rest 2 are ignored. But again when i search for those 5 records in the same window, all of them are plotted !

    I have added canvas modules, in $(document).ready state, as shown in below code block

    //REGISTER the custom pushpin module
    Microsoft.Maps.registerModule(“CanvasPushpinModule”, “scripts/CanvasPushpinModule.js”);
    //load the pushpin module
    Microsoft.Maps.loadModule(‘CanvasPushpinModule’);

    Below function is where i get “document.getElementById(canvasID)” as “null”

    function getCanvas(canvasID) { var c = document.getElementById(canvasID);
    if (c) { c = c.getContext(“2d”); }
    return c; }

    Anything that i’m missing here ? Please suggest.

    • After investigation further we’ve found the root cause of the problem. Let us explain the scenario a bit just to have a clear background of what our problem was and how we resolved it.

      We have geo-coded records that can be plotted on the map by selecting them from the Home Page or by a search method from the Map Page. We were setting the view/zoom level of the map as per the record collection i.e. if there was only one record we would zoom into that only record on the map, if there are many records in the collection we then set the appropriate map view/zoom level to display all of them.
      Now when we used to select a few records from Home Page and call the Map Page everything used to work fine. The problem of getting canvas element as null used to occur when we used to search for other records from the Map Page with those selected records already plotted on the map. This behavior/problem doesn’t used to occur if we were on the Map Page with no canvas pushpins plotted on the map.

      After digging into the code and lots of hard work we found the lines that were creating the problem for us.

      var viewRect = getLocationBounds();
      _map.setView({ bounds: viewRect });

      // Method to get the location bounds to set the map view.
      function getLocationBounds() {

      var rect = null;
      var latLong = new Array();
      var functionName = “getLocationBounds “;

      try {

      //loop through collection and add into collection
      $.each(_allRecords, function (index, data) {

      if (data.attributes[Latitude] != undefined && data.attributes[Longitude] != undefined) {
      //add the location to the list
      latLong.push(new Microsoft.Maps.Location(data.attributes[Latitude].value, data.attributes[Longitude].value));
      }
      });

      ////Check if locations found
      if (latLong.length > 0) {
      rect = new Microsoft.Maps.LocationRect.fromLocations(latLong);
      }
      }
      catch (e) {
      throw new Error(e);
      }
      return rect;
      }

      Here we used to iterate through the records and set the MapView, using the Map control. After commenting

      var viewRect = getLocationBounds();
      _map.setView({ bounds: viewRect });

      These two lines the problem what we were facing went.

      Since we used the Bing method to set the View of the map we faced problem which took us very long to resolve.

      We are posting our answer here in case it may help someone in future.

      We would like to know if you have any suggestions for us that could justify this behavior of Bing Map. We would like to know how to achieve the setView functionality, as shown in our above code.

  4. Hi,
    I am using custom canvas colour push pin it is working.
    However if pushpins are close to each other click event overlaps.
    With f12 I am seeing canvas element is mush bigger then base element.
    I have tried to fix it with css
    canvas
    {
    height: inherit;
    width: inherit;
    }
    Then it gets as the base element but then image gets so small.
    If you could let me know how I can fix it that would be great.
    Thanks

    • Look at the samples. You should see that when using an image it changes the size of the canvas to size of the image. If you are not using an image, set the size in code anyways. If you are using images, make sure that there isn’t a bunch of empty space around the image, as transparent parts of an image are still considered part of the image.

  5. i tried to add a click event to the canvas pushpin but it won’t work. can you please add an example on how to do that?

  6. those links were very helpful. Thank you for that. One Last question, is it possible to have two pie chart canvas push pins right on top of each other. where the under laying canvas has a pie chart push pin with slices from 0 – 1* PI and the top canvas pie chart has slices from 1* PI – 0. The tricky part is clicking through the top canvas so that when the slices of the underlying canvas are clicked, animation occurs. I was wondering if it is possible to do such a thing. The two Canvas pie chart pins will be exactly at the same coordinates. at the end, the slices will look as if they are part of the same pie chart, but they are two different canvas pins and the slices are split among two canvas pie charts.

Leave a comment