Thursday, January 23, 2014

Generating server-side tile-maps with Node.JS (Part 1 - Programatically with Canvas)

Part 1 - Programatically with Canvas
Part 2 - Using Mapnik

This will be a series of posts on leveraging Node.js to generate tile-images on server-side that will be displayed on top of Bing Maps (although applicable to any other mapping API).

For those not familiar with it, Node has gained lots of momentum on the web. It provides a simple event oriented, non-blocking architecture on which one can develop fast and scalable applications using Javascript.

It lends itself really well for generating tile-images and is in-fact being used by some large players like Mapbox.

On this first post I'm going to generate very simple tiles. I won't fetch data from the database nor do anything too fancy. Each tile will simply consist on a 256x256 png with a border and a label identifying the tile's z/x/y. Conceptually something like this:


I'm going to use an amazing Node module called Canvas which provides the whole HTML5 Canvas API on server-side. My main reason is that it's an API that I know particularly well and I can reuse lots of my existing code.

I'm going to develop on Mac OS X but most of this would be exactly the same on Windows or Linux.

Afterwards, as an extra, I'm also going to provide instructions on how to setup a Linux Virtual Machine in Azure running the resulting node application.

First things first. I'll setup an bare-bones node application and gradually add more stuff to it.
Install Node and create a file named server.js with the following content:
console.log("Hello World");
Run this node application in a terminal window:
node server.js

Not particularly impressive but we now know that Node is working. To return an image I'll use, as mentioned, the Canvas module. Installing the module is simple enough (npm install canvas), but the problem is that it has a dependency on a lib called Cairo. Installing Cairo  requires one to follow a couple of installation steps religiously (includes information for Windows, Linux, Mac OS X, et al.)
https://github.com/LearnBoost/node-canvas/wiki/_pages

After installing Cairo and the Canvas module we're ready to start. Let's just generate a simple image on the fly and return it as a server response.

Update the node server to return an image generated on the fly with Canvas:
var http = require('http');
var Canvas = require('canvas');

var server = http.createServer(function (request, response) {

    var canvas = new Canvas(256, 256)
    var context = canvas.getContext('2d');

    context.beginPath();
    context.rect(0, 0, 256, 256);

    context.lineWidth = 7;
    context.strokeStyle = 'black';
    context.stroke();

    var stream = canvas.createPNGStream();

    response.writeHead(200, {"Content-Type": "image/png"});
    stream.pipe(response);

});

server.listen(8000);
After running this server and pointing the browser to localhost:8000 a simple 256x256 image should be displayed.

In Node, to create an HTTP Server, a module named Express is invaluable. It provides loads of functionality including a routing engine that I'll use in this demo to obtain the tile's zxy and display it inside the square. You'll have to install it using npm.
npm install express
Update the server to parse the request and paint a text with the Z, X, Y
var express = require('express');
var Canvas = require('canvas');
var app = express();

app.get('/:z/:x/:y', function(req, res) {

    var z = req.params.z,
        x = req.params.x,
        y = req.params.y;

    var canvas = new Canvas(256, 256)
    var context = canvas.getContext('2d');

    context.beginPath();
    context.rect(0, 0, 256, 256);

    context.lineWidth = 7;
    context.strokeStyle = 'black';
    context.stroke();

    context.fillStyle = "darkblue";
    context.font = "bold 16px Arial";
    context.fillText(z + "/" + x + "/" + y, 100, 128);


    var stream = canvas.createPNGStream();

    res.type("png");
    stream.pipe(res);

});

app.listen(process.env.PORT || 8000);

When running the server it will use the url parameters as http://localhost:8000/z/x/y and display them inside the square:

The last step will be creating an Html page with a Bing Map that uses these tiles.

index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <title>Bing Maps Server Tile Layer - Simple Demo</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body onload="loadMap();">

<div id='mapDiv' style="width:100%; height: 100%;"></div>

<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
<script type="text/javascript">

    function loadMap()
    {
        var MM = Microsoft.Maps;
        var map = new MM.Map(document.getElementById("mapDiv"), {
            center: new MM.Location(39.5, -8),
            mapTypeId: MM.MapTypeId.road,
            zoom: 7,
            credentials:"YOUR CREDENTIALS HERE"});

        var tileSource = new MM.TileSource({ uriConstructor:  function (tile) {

            var x = tile.x;
            var z = tile.levelOfDetail;
            var y = tile.y;

            return "http://localhost:8000/" + z + "/" + x + "/" + y;
        }});

        var tileLayer = new MM.TileLayer({ mercator: tileSource});
        map.entities.push(tileLayer);
    }

</script>
</body>
</html>
A map should appear with an additional layer with the generated tiles.

You can check the end-result here.

Extra:

I've deployed the above example in a Linux Virtual Machine on Azure. I'm going to show you the exact steps that I took to do so.






On the Azure Portal press "Add":





Choose "Virtual Machine"












Choose Ubuntu Server 12.04 LTS from the UBUNTU section.














Fill the details. The Virtual Machine Name will have to be something unique. You'll be assigned an URL like <virtual machine name>.cloudapp.net.






Instead of using a SSH Key I would suggest, for simplicity, to just set a user name and a password.











The wizard then asks which ports you want open. The SSH appears by default and is mandatory to be able to manage the machine. I've also configured two HTTP endpoints and one for FTP.


After finishing the process it should take a couple of minutes to finalise the Virtual Machine.
Afterwards use a SSH client to access the machine (putty on windows or ssh on Mac OS X):

ssh <virtual machine name>.cloudapp.net -l <user>
For my particular example:
ssh build-failed.cloudapp.net -l pedro

The first thing to do inside the machine would be to install Node.

Just run the following commands as recommended on http://kb.solarvps.com/ubuntu/installing-node-js-on-ubuntu-12-04-lts/.
root@nodepod:~# sudo apt-get update
root@nodepod:~# sudo apt-get install python-software-properties python g++ make
root@nodepod:~# sudo add-apt-repository ppa:chris-lea/node.js
root@nodepod:~# sudo apt-get update
root@nodepod:~# sudo apt-get install nodejs 
I also had to install Cairo according to the instructions at: https://github.com/LearnBoost/node-canvas/wiki/Installation---Ubuntu
sudo apt-get update 
sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
Now, to copy the files I've developed previously on this post I'm going to setup an FTP server on the Virtual Machine. I've used the page at https://help.ubuntu.com/12.04/serverguide/ftp-server.html as reference:
sudo apt-get install vsftpd
I will have the FTP configured as:
  • Each local user will have access to the FTP
  • Anonymous access is not allowed
  • Write permission is allowed
Thus I've edited the configuration at "/etc/vsftpd.conf" (sudo nano /etc/vsftpd.conf) with:
anonymous_enable=NO
local_enable=YES
write_enable=YES
Now just restart the FTP server and you should be set to go:
sudo restart vsftpd
Now just copy the files over to the server and execute the app with a "&" for it to run in the background.
node server.js &
I've actually updated the server.js code slightly so that it includes:
- compression
- cache headers
- a route to serve the index.html file.

The complete server.js should be:
var express = require('express');
var Canvas = require('canvas');
var app = express();

app.use(express.compress());

app.get('/', function(req,res) {
    res.sendfile('public/index.html');
});

app.get('/:z/:x/:y', function(req, res) {

    res.setHeader("Cache-Control", "max-age=31556926");

    var z = req.params.z,
        x = req.params.x,
        y = req.params.y;

    var canvas = new Canvas(256, 256)
    var context = canvas.getContext('2d');

    context.beginPath();
    context.rect(0, 0, 256, 256);

    context.lineWidth = 7;
    context.strokeStyle = 'black';
    context.stroke();

    context.fillStyle = "darkblue";
    context.font = "bold 16px Arial";
    context.fillText(z + "/" + x + "/" + y, 100, 128);

    var stream = canvas.createPNGStream();

    res.type("png");
    stream.pipe(res);

});

app.listen(process.env.PORT || 8000);

6 comments:

  1. Muito bom ... obrigado pela partilha

    ReplyDelete
  2. This is awesome,
    Thank you so much for this tutorial,
    :)

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. wow, i'm looking for this tutorial, you explained very well..
    i try this tutorial and works 100% thanks

    ReplyDelete
  5. We’ve been stumbling around the internet and found your blog along the way.

    We love your work! What a great corner of the internet :)


    pave tile - official website

    ReplyDelete