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