Tuesday, February 21, 2012

Leaflet and HTML5

When one thinks of web map platforms Google Maps and Bing Maps are the two obvious choices. They're both free as long as you comply with some ground rules (i.e, terms of use) and don't exceed their usage limit. Unfortunately many applications can't adhere to those restrictions, leaving them with two options: purchase a commercial license or use a free alternative.

The most common free alternative is, without a doubt, OpenLayers. It provides an incredibly complete API (much more than Google and Bing) and integration with varied data sources (see examples here). The problem with OpenLayers, in my opinion, is it complexity. I'm also not particularly fond of the map control itself, but that's probably just personal taste.

Anyway, recently appeared a new kid on the block: Leaflet. It is a free lightweight mapping solution that provides a rich and extensible API, very much focused on HTML5. Also, it includes something that a lot of developers are asking for in Bing Maps V7: a client-side canvas layer. In this post I'm going to show how it works and develop a working prototype.

Update (21/01/2013): I've added a working example here

The example will be very similar to the one in my post Bing Maps and HTML5 (Part 1). The main difference will be that the points are going to be created using HTML5 Canvas instead of pushpins. I've already done that in another of my posts, so what's really the difference here? The difference is that, instead of drawing a canvas above the map, we're painting a tile-layer manually. That might seem confusing so let me try to rephrase it:

Leaflet allows the composition of several layers. In this case there will be 2: the base map and the 18 districts of Portugal, represented as numbered circles.

The base map shows the roads, cities, forests, etc, and is based on OpenStreetMap (I'll be blogging soon about OSM).

This base map, like Google Maps and Bing Maps, is tile-based. Meaning that a map view is not composed of a single image but from a set of tiles, placed in a grid-like structure. Each tile has a different image and new tiles are loaded when moving/zooming the map.

It is very common to generate tiles on the server which are fetched as images and displayed as overlays in the map. Leaflet provides a new alternative: tiles generated at client side. The behavior is very similar to the ones at server-side, but a callback is provided to the developer so that he may "paint" the tile using HTML5 Canvas.

This is the skeleton of the implementation:
map = new L.Map('map');
map.setView(new L.LatLng(39.5, -8.5), 6);

var tiles = new L.TileLayer.Canvas();
tiles.drawTile = function (canvas, tile, zoom) {
    var context = canvas.getContext('2d');
    //paint tile
}

map.addLayer(tiles);
Obviously it will not be that simple, as there is some math to convert WGS84 coordinates to the pixel coordinates of the tile. Anyway, the full implementation for the circles that appear in the image is:
var tiles = new L.TileLayer.Canvas();

tiles.drawTile = function (canvas, tile, zoom) {
    var context = canvas.getContext('2d');

    // circle radius
    var radius = 12;

    var tileSize = this.options.tileSize;

    for (var i = 0; i < points.length; i++) {

        var point = new L.LatLng(points[i].lat, points[i].lon);

        // start coordinates to tile pixels
        var start = tile.multiplyBy(tileSize);

        // actual coordinates to tile pixel
        var p = map.project(point);

        // point to draw
        var x = Math.round(p.x - start.x);
        var y = Math.round(p.y - start.y);

        // Circle
        context.beginPath();
        context.arc(x, y, radius, 0, 2 * Math.PI, false);

        // Fill (Gradient)
        var grd = context.createRadialGradient(x, y, 5, x, y, radius);
        grd.addColorStop(0, "#8ED6FF");
        grd.addColorStop(1, "#004CB3");
        context.fillStyle = grd;

        // Shadow
        context.shadowColor = "#666666";
        context.shadowBlur = 5;
        context.shadowOffsetX = 7;
        context.shadowOffsetY = 7;
        context.fill()

        context.lineWidth = 2;
        context.strokeStyle = "black";
        context.stroke();

        // Text
        context.lineWidth = 1;
        context.fillStyle = "#000000";
        context.lineStyle = "#000000";
        context.font = "12px sans-serif";
        context.textAlign = "center";
        context.textBaseline = "middle";
        context.fillText(i + 1, x, y);

    }
}
Also, and like with my previous post, I've included a video showing the usage of this map.


Update (21/01/2013): I've added a working example here



10 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi Pedro,

    interesting sample. But one question: if you print the map, the blue circles printed also?

    Thanks.
    Marcus

    ReplyDelete
  3. Where are you getting tile.multiplyBy() ?

    TypeError: Object Point(2, 1) has no method 'multipleBy'

    ReplyDelete
  4. Is the base layer (OSM) generated on the client or server side?

    From what you wrote in the post I understood you generate it on the client side using canvas, but the code only references the circles and not the base layer.

    Thanks
    Gil

    ReplyDelete
  5. Hi Gil,

    The base layer is an OSM tile layer (server-side). The code is incomplete and just shows the canvas tile layer. I'll try to include a running example of this example on the post.

    ReplyDelete
  6. I've just added a working example on the post. You can check it at: http://psousa.net/demos/html5leaflet/

    ReplyDelete
  7. Hey Pedro, this is pretty cool. What advantage does this have over using individual markers (push pins) on the map? Is it faster for the client side or anything like that?

    ReplyDelete
  8. For this example in particular the main advantage is the ability to create dynamic markers (although one could generate them dynamically in server-side). But where I'm showing simple markers there could be complex polygons/polygons, which provides some additional possibilities

    ReplyDelete
  9. Hi Pedro, thank you for your answer.I try to create polygon but it is very slow to show (I have about 100000 polygon in the map)is it possible to show me how to implement polygon.

    ReplyDelete