Howto real time tiles rendering with mapnik and mod python
In a project I was creating, based on my own rendering rules sets, I hit a problem that I solved by real-time rendering tiles. It is clearly not the fastest solution as mod_tile is better. But I didn't want to install mod_tile
Here is a live example of it : Yet another validation tool for osm data
What is this solution good for?
- If you wish to save disk space
- If you want to test or to have several different mapnik styles
- If you want more flexibility on tile generation process with your own python code
- If you want updates very often
- if you can't or don't want to install mod_tile which is faster
What problems does that create?
- If more than ~2 or 3 users access maps at the same time, it will put a lot of CPU load on the machine (for massive use, a cache mecanism is available, and you'll be able to serve more users )
- Generation times are quite slow
- On low zoom (3 to 10) tile generation time might be even worse (a cache is definitively useful for those zoom levels)
Hardware machine for my rendering
I'm using a fairly "standard" machine, and I expect things to be a lot lot better if you have a powerful server. Composed of :
- Intel(R) Core(TM)2 Duo CPU E7200 @ 2.53GHz
- 2 GO RAM
- 512 Go disk space (I'm currently using 100 Mo of it for the permanent tiles cache)
software configuration
- GNU/Linux Debian Lenny amd64
- postgres & postgis
- osm2pgsql
Starting up
I won't write everything again, so please have a look at the very good tutorials :
- Mapnik to prepare things
- Mapnik/Installation
- Mapnik/PostGIS Install with postgis integration
- Osm2pgsql compile it (package are outdated for my system ) to import the data
Then you have to try some generation with python of tiles to make sure every things work with mapnik (generate_image.py is a good one to test )
How to set up the real time rendering
Prerequist and line of installation
This method needs mod_python included with apache in order to limit loading and unloading the python interpreter
The idea is that you will define a "virtual" tile directory that will in fact be empty where the python script that does all the work is.
Preparing the directory
Create a directory of your choice ("tiles" in my example) and write a .htaccess file with :
SetHandler python-program PythonPath "['/home/sites/www.openhikingmaps.org/tiles/'] + sys.path" PythonHandler renderer::handle ExpiresActive on ExpiresDefault "access plus 1 hours"
- /home/sites/www.openhikingmaps.org/tiles/ is the path of you directory
- ExpiresDefault "access plus 1 hours" will tell that tiles should be kept by the browser in cache for at least 1 hour
- PythonHandler renderer::handle says that to do the tile rendering should be handled by a file called "renderer.py" with a method "handle"
Put the python script in
In that directory put a file called renderer.py that contains :
#!/usr/bin/env python
# Source is GPL, credit goes to Nicolas Pouillon
# comments goes to sylvain letuffe org (" " are replaced by @ and . )
# fichier execute par mod_python. handle() est le point d'entree.
import os, os.path
from gen_tile import MapMaker
def get_renderers(path, zmax = 20):
r = {}
for filename in os.listdir(path):
if filename.startswith("."):
continue
if not filename.endswith(".xml"):
continue
rname = filename[:-4]
try:
r[rname] = MapMaker(os.path.join(path, filename), zmax)
except:
pass
return r
zmax=20
renderers = get_renderers("/home/sites/www.openhikingmaps.org/mapnik-styles/", zmax)
def handle(req):
from mod_python import apache, util
path = os.path.basename(req.filename)+req.path_info
# strip .png
script, right = path.split(".", 1)
new_path, ext = right.split(".", 1)
rien, style, z, x, y = new_path.split('/', 4)
if style in renderers:
req.status = 200
req.content_type = 'image/png'
z = int(z)
x = int(x)
y = int(y)
#req.content_type = 'text/plain'
#req.write(renderers[style])
#return apache.OK
if z<13:
cache=True
else:
cache=False
req.write(renderers[style].genTile(x, y, z, ext, cache))
else:
req.status = 404
req.content_type = 'text/plain'
req.write("No such style")
return apache.OK
You have to change /home/sites/www.openhikingmaps.org/mapnik-styles/ to where you mapnik styles are
Note : If you are using an other webserver than Apache and you could not use the mod_python module, here is a script which works with lighttpd (fcgi and flup packages needed) :
#!/usr/bin/env python
# Source is GPL, credit goes to Nicolas Pouillon
# comments goes for this version to pierre_dot_mauduit _at_ gmail _dot_ com
import os, os.path
from gen_tile import MapMaker
def get_renderers(path, zmax = 20):
r = {}
for filename in os.listdir(path):
if filename.startswith("."):
continue
if not filename.endswith(".xml"):
continue
rname = filename[:-4]
try:
r[rname] = MapMaker(os.path.join(path, filename), zmax)
except:
pass
return r
zmax=20
renderers = get_renderers("/var/www/rtmapnik/", zmax)
def handle(environ, start_response):
newpath, ext = os.environ['PATH_INFO'].split('.', 1)
rien, style, z, x, y = newpath.split('/', 4)
if style in renderers:
status = 200
content_type = 'image/png'
z = int(z)
x = int(x)
y = int(y)
if z<13:
cache=False
else:
cache=False
start_response('200 OK', [('Content-Type', content_type)])
print renderers[style].genTile(x, y, z, ext, cache)
return []
else:
return ["Error occured"]
if __name__ == '__main__':
from flup.server.fcgi import WSGIServer
WSGIServer(handle).run()
And another one, in the same directory with a name "gen_tile.py"
#!/usr/bin/env python
import os
import mapnik
import math
def minmax (a,b,c):
a = max(a,b)
a = min(a,c)
return a
class GoogleProjection:
def __init__(self,levels=18):
self.Bc = []
self.Cc = []
self.zc = []
self.Ac = []
c = 256
for d in range(0,levels):
e = c/2;
self.Bc.append(c/360.0)
self.Cc.append(c/(2 * math.pi))
self.zc.append((e,e))
self.Ac.append(c)
c *= 2
def fromLLtoPixel(self,ll,zoom):
d = self.zc[zoom]
e = round(d[0] + ll[0] * self.Bc[zoom])
f = minmax(math.sin(math.radians(ll[1])),-0.9999,0.9999)
g = round(d[1] + 0.5*math.log((1+f)/(1-f))*-self.Cc[zoom])
return (e,g)
def fromPixelToLL(self,px,zoom):
e = self.zc[zoom]
f = (px[0] - e[0])/self.Bc[zoom]
g = (px[1] - e[1])/-self.Cc[zoom]
h = math.degrees( 2 * math.atan(math.exp(g)) - 0.5 * math.pi)
return (f,h)
class MapMaker:
sx = 256
sy = 256
prj = mapnik.Projection("+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over")
def __init__(self, mapfile, max_zoom):
self.m = mapnik.Map(2*self.sx, 2*self.sy)
self.max_zoom = max_zoom
self.gprj = GoogleProjection(max_zoom)
try:
mapnik.load_map(self.m,mapfile)
except RuntimeError:
raise ValueError("Bad file", mapfile)
self.name = hex(hash(mapfile))
def tileno2bbox(self, x, y, z):
p0 = self.gprj.fromPixelToLL((self.sx*x, self.sy*(y+1)), z)
p1 = self.gprj.fromPixelToLL((self.sx*(x+1), self.sy*y), z)
c0 = self.prj.forward(mapnik.Coord(p0[0],p0[1]))
c1 = self.prj.forward(mapnik.Coord(p1[0],p1[1]))
return mapnik.Envelope(c0.x,c0.y,c1.x,c1.y)
def genTile(self, x, y, z, ext="png", cache=False):
if cache:
outname = '/home/sites/www.openhikingmaps.org/tiles/cache/%d/%d/%d.%s'%( z, x, y, ext)
if os.path.exists(outname):
fd = open(outname, 'r')
return fd.read()
try:
os.makedirs(os.path.dirname(outname))
except:
pass
else:
outname = os.tmpnam()
bbox = self.tileno2bbox(x, y, z)
bbox.width(bbox.width() * 2)
bbox.height(bbox.height() * 2)
self.m.zoom_to_box(bbox)
im = mapnik.Image(self.sx*2, self.sy*2)
mapnik.render(self.m, im)
view = im.view(self.sx/2, self.sy/2, self.sx, self.sy)
view.save(outname, ext)
fd = open(outname)
out = fd.read()
fd.close()
if not cache:
os.unlink(outname)
return out
# Fonction de test, qui n'est appelee que si on execute le fichier
# directement.
def test():
renderer = MapMaker('/home/sites/www.openhikingmaps.org/mapnik-styles/test.xml', 18)
for filename, x, y, z in (
('test.png', 2074, 1409, 12),
):
fd = open(filename, 'w')
fd.write(renderer.genTile(x, y, z))
fd.close()
if __name__ == '__main__':
test()
Again replace /home/sites/www.openhikingmaps.org/tiles/ by your directory.
As you can see, if you read python fluently, here :
cache = z<13
req.write(renderers[style].genTile(x, y, z, ext, cache))
This is a piece of code that says to keep cached files for zoom <13 you can adapt if you wish to "<0" for no cache at all or "<20" for a cache of anything.
<13 looks a good compromise because, low zoom tiles take a long time to create, while keeping everything will need to flush at some times (based on tiles dates, osm2pgsql diff expired tiles list or at defined dates )
Open Layers
I started form there : OpenLayers Simple Example
And added the relevant parts in the index :
<script src="/libs/OpenLayers.js"></script>
<script src="/libs/styles.js"></script>
(...)
var layers = [];
for ( var idx in all_available_styles ) {
var name = all_available_styles[idx];
var l = new OpenLayers.Layer.TMS(
name,
["/tiles/renderer.py/"+idx+"/"],
{type:'jpeg',
getURL: get_osm_url,
displayOutsideMaxExtent: true }, {'buffer':1} );
layers.push(l);
}
map.addLayers(layers);
My full index.html file for reference
With /libs/OpenLayers.js being a local copy.
And /libs/styles.js containing the different possible styles that you want to use.
var all_available_styles= new Array(); all_available_styles["noname"]=["No name"]; all_available_styles["nooneway"]=["No Oneway"]; all_available_styles["noref"]=["No Ref on way"]; all_available_styles["fixme"]=["Fixme or note Tags"]; all_available_styles["for_wikipedia"]=["For Wikipedia (without POI)"]; all_available_styles["mapnik_copy"]=["Mapnik SVN Copy (11/2008)"]; all_available_styles["hiking"]=["Hiking"]; all_available_styles["test_zone"]=["test zone"]; all_available_styles["test_zone2"]=["test zone 2"];
- the key is the name of your mapnik style file (without .xml )
- The value is the name that is displayed on the layer switcher
Cron job
My simplified cron job, takes its data from http://download.geofabrik.de/osm/ and looks likes this : It must be launched by the postgres user or a user that has access to the DB :
- populate_db.sh
#!/bin/bash cd /home/sites/www.openhikingmaps.org/cron #File name FILE=france.osm.bz2 # URL where to find it URL=http://download.geofabrik.de/osm/europe #remove old rm $FILE #get the new wget $URL/$FILE /usr/local/bin/osm2pgsql -S ./default.style -m -d gis $FILE 2>>log >>/dev/null
- "-S ./default.style" is optionnal but you can write your own if you wish
edit 2009-03-20 : This solution was the easy one but importing europe is long, very long, I now use diff imports as explained here Minutely_Mapnik
Tile URL acccess
In those examples, the URL for the tile access is constructed to be :
- noname being the name of the style file you want to use for the rendering
- 12 being the zoom level
- jpeg being the chosen extension, just change it to png and you will get png !
PS: you could remove the "renderer.py" as it is useless and adapt the split code of the url, but it at least tells people that this uses python and not mod_tile
Thanks for reading. -- Sletuffe 15:08, 25 November 2008 (UTC)
Tweeks to my setup
Questions / remark
On the talk page, or by e-mail please