WMS and SVG Input with Mapnik and Cairo
A rather techy post from me, just before the Final Project Post for GEMMA (my current project at CASA) is submitted, summing the project and applications up. Why? I wanted to share with you two rather specialist bits of GEMMA that required quite a bit of head-scratching and trial-and-error, in the hope that, for some developer out there, they will be of use in their own work. I’m cross-posting this from the GEMMA project blog.
One of the core features of GEMMA that I have always desired is the ability to output the created map as a PDF. Not just any PDF, but a vector-based one – the idea that it will be razor-sharp when you print it out, rather than just looking like a screen grab. I had written a basic PDF creator, using Mapnik and Cairo, for OpenOrienteeringMap (OOM), an earlier side-project, and because the GEMMA project is about embracing and extending our existing technologies and knowledge at CASA, rather than reinventing the wheel, I was keen to utilise this code in GEMMA.
Most of the layers were quite straightforward – the two OpenStreetMap layers (background and feature) are very similar indeed to OOM, while the Markers and Data Collector layers were also reasonably easy to do – once I had imported (for the former) and hand-crafted (for the latter) suitable SVG images, so that they would stay looking nice when on the PDF. The trickiest layer was the MapTube layer. For the terms of this project, the MapTube imagery is not a vector layer, i.e. we are not using WFS. However I was still keen to include this layer in the PDF, so I turned to Richard Milton (the creator of MapTube) and discovered there is an WMS service that will stitch together the tiled images and serve them across the net. I could combine this requesting the WMS images on the GEMMA server (not the client!), converting them to temporary files, and then using a RasterSymbolizer in Mapnik 2, and an associated GDAL filetype.
The trickiest part was setting georeferencing information for the WMS image. Georeferencing is used to request the image, but it is also needed to position the image above or below the other Mapnik layers. Initially it looked like I would have to manually create a “worldfile”, but eventually I found a possibly undocumented Mapnik feature which allows manual specification of the bounding box.
I’ve not seen this done anywhere else before, although I presume people have just done it and not written it down on the web, so here’s my take, in Python.
First we get our WMS image. MAP_W and MAP_H are the size of the map area on the “sheet” in metres. We request it with a resolution of 5000 pixels per metre, which should produce a crisp looking image without stressing the server too much.
mb = map.envelope() url = maptube_wms_path + "/?request=GetMap&service=WMS&version=1.3.0" url = url + &format=image/png&crs=EPSG:900913" url = url + "&width=" + str(int(MAP_W*5000)) url = url + "&height=" + str(int(MAP_H*5000)) url = url + "&bbox=" + str(mb.minx) + "," + str(mb.miny) + "," url = url + str(mb.maxx) + "," + str(mb.maxy) url = url + "&layers=MAPID" + str(maptubeid) furl = urllib2.urlopen(url, timeout=30)
Mapnik doesn’t work directly with images, but files, so we create a temporary file:
ftmp = tempfile.NamedTemporaryFile(suffix = '.png') filename = ftmp.name ftmp.write(furl.read())
Next we set up the layer and style. It’s nice that we can pass the opacity, set on the GEMMA website, straight into the layer in Mapnik.
style = mapnik.Style() rule = mapnik.Rule() rs = mapnik.RasterSymbolizer() rs.opacity = opacity rule.symbols.append(rs) style.rules.append(rule) map.append_style(filename,style) lyr = mapnik.Layer(filename)
Here’s the key step, where we manually provide georeferencing information. epsg900913 is the usual Proj4 string for this coordinate reference system.
lyr.datasource = mapnik.Gdal(base='',file=filename, bbox=(mb.minx, mb.miny, mb.maxx, mb.maxy)) #Override GDAL lyr.srs = epsg900913
Finally:
lyr.styles.append(filename) map.layers.append(lyr)
I’m excited about one other piece of code in the PDF generation process, as again it involves jumping through some hoops, that are only lightly documented – adding an SVG “logo” – the SVG in this case being the GEMMA gerbil logo, that Steve (co-developer) created from Illustrator. Cairo does not allow native SVG import (only export) but you can use the RSVG Python package to pull this in. I’m being a bit lazy in hard-coding widths and scales here, because the logo never changes. There are more sophisticated calls, e.g. svg.props.width, that could be useful.
svg = rsvg.Handle(gemma_path + "/images/logo.svg") ctx = cairo.Context(surface) ctx.translate((MAP_WM+MAP_W)*S2P-ADORN_LOGO_SIZE, CONTENT_NM*S2P) ctx.scale(0.062, 0.062) svg.render_cairo(ctx)
Note that we are calling render_cairo, a function in RSVG, rather than a native Cairo function that we do for all the other layers in the PDF.
The screenshot above contains data from the OpenStreetMap project.