
Here's a quick howto for local map rendering, using
python,
mapnik
and data from
OpenStreetMap.
This tutorial shows how to create a stylesheet for mapnik from scratch, if
you're looking for how to tweak OpenStreetMap's mapnik rendering, I plan
to write a tutorial on
PostGIS-driven rendering at a later time.
This post is not for those who want to make a local slippy map, nor those
who want to render different areas of big dumps (think of big national dumps).
We'll render some features of a residential area of a city with buildings
mapped. We'll get a
small dump of the area for this, using the
XAPI.
First, you need to install mapnik. On Debian-like systems, it should be as
easy as:
# apt-get install python-mapnik
Then, you need some python code to use the mapnik library. Let's call it
render.py:
#!/usr/bin/python
import mapnik
###
# Configuration
###
style = 'style.xml'
output = 'output.svg'
width = 1280
height = 800
bbox = '12.58664,37.65759,12.5945,37.66325'
###
# Don't touch below!
###
bbox = bbox.split(',')
ll = mapnik.Coord(float(bbox[0]), float(bbox[1]))
tr = mapnik.Coord(float(bbox[2]), float(bbox[3]))
mymap = mapnik.Map(width, height)
mapnik.load_map(mymap, style)
map_bbox = mapnik.Envelope(ll, tr)
mymap.zoom_to_box(map_bbox)
mapnik.render_to_file(mymap, output)
For the purposes of this post, we're going to create a 1280x800 map -- adjust
that to suit your needs -- and we specified a
bounding box corresponding to
that area. That's composed as west,south,east,north coordinates. Be sure to
get them right :-).
Now we need to write a
Mapnik stylesheet, we'll call it
style.xml. That's
a XML document, you can find the documentation
at the official website.
The skeleton of a stylesheet is as follows:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Map>
<Map bgcolor="lightblue" srs="+proj=latlong +datum=WGS84">
<Style name="...">
...
</Style>
<Layer name="...">
<Datasource>
...
</Datasource>
</Layer>
</Map>
The stylesheet has an arbitrary number of
Style elements
and an arbitrary number of
Layer elements. The former class of elements
define the appearance of the features, while the latter defines where to get
data from.
Each layer has a
Datasource child: here's where you say where
to get your data from. We're going to use the osm input plugin, which
lets you parse the dump directly. This has some drawbacks:
- not scalable for big dumps
- for each bbox you want to render, the full dump has to be re-parsed
But works great for small dumps.
In this tutorial, we want to pretty render a residential area, so we first
need to render the road network. This will mainly be made of
highway=residential roads.
Each
style can have one or more
Rule element. These define what
elements that particular style should apply to. Each rule can have different
rendering settings.
The given dump has different roads: residential, unclassified, tertiary and
secondary. Let's add a rule for each. Here's the first of them:
<Rule>
<Filter>[highway] = 'residential'</Filter>
<LineSymbolizer>
<CssParameter name="stroke">#ffffff</CssParameter>
<CssParameter name="stroke-width">5</CssParameter>
<CssParameter name="stroke-linejoin">round</CssParameter>
<CssParameter name="stroke-linecap">round</CssParameter>
</LineSymbolizer>
<TextSymbolizer name="name" face_name="DejaVu Sans Book" size="8" fill="#000" halo_radius="1" spacing="300" placement="line" />
</Rule>
It is quite straightforward: you define what elements the rule should apply
to, and then add some
symbolizer. For a detailed description of each
symbolizer, please read the
documentation.
The example dump also has a railway and some steps. To render them, we use
the
stroke-dasharray property:
<Rule>
<Filter>[highway] = 'steps'</Filter>
<LineSymbolizer>
<CssParameter name="stroke">#ff0000</CssParameter>
<CssParameter name="stroke-width">5.0</CssParameter>
<CssParameter name="stroke-dasharray">2,1</CssParameter>
</LineSymbolizer>
</Rule>
<Rule>
<Filter>[railway] = 'rail'</Filter>
<LineSymbolizer>
<CssParameter name="stroke">#222222</CssParameter>
<CssParameter name="stroke-width">3</CssParameter>
<CssParameter name="stroke-linejoin">round</CssParameter>
</LineSymbolizer>
<LineSymbolizer>
<CssParameter name="stroke">white</CssParameter>
<CssParameter name="stroke-width">1</CssParameter>
<CssParameter name="stroke-linejoin">round</CssParameter>
<CssParameter name="stroke-dasharray">10,20</CssParameter>
</LineSymbolizer>
</Rule>
Also here, it is quite simple. The values given to stroke-dasharray determine
the size of the dashes, and they can be alternated. So, for the steps we have
2px of #ff0000 and 1px of trasparency, while for the rail we have 10px of
white and 20px of #222222.
What if we want to add some markers to the map? Easy, just use another
symbolizer, called
PointSymbolizer:
<Rule>
<Filter>[highway] = 'stop'</Filter>
<PointSymbolizer width='10' height='22' file='stop.png' type='png' allow_overlap='true' opacity='1.0' />
</Rule>
Here we mark stop signals with a pre-rendered stop.png (only png and tiff are
currently supported, and svg support is only in the development version).
What to do next? Well, we can make this map
really nice. We'll render
buildings with a pseudo-3D effect, which, however, does not reflect real
heights -- but it's rather nice to see. How to achieve this?
BuildingSymbolizer is the answer.
<Rule>
<Filter>[building] <> ''</Filter>
<BuildingSymbolizer>
<CssParameter name="fill">#00ff00</CssParameter>
<CssParameter name="fill-opacity">0.4</CssParameter>
<CssParameter name="height">0.00007</CssParameter>
</BuildingSymbolizer>
</Rule>
Here we just draw
building=*. Regarding the height, you need to play a bit
-- it took a while before I found a good value of it.
One thing missing in this stylesheet, is the rendering of coastlines. Since
we don't have a coastline here, I'm skipping it, but the easy way is: use
another rule. This is really just a trick; OpenStreetMap renders the coastline
in a different way respect to other features, that's why I'm using this
trick.
I'm not posting the whole stylesheet, but you can
download it. You can
also get
the script,
the OSM dump and the
stop.png image.
Now, you should be able to just do:
$ ./render.py
and you will have your map in
output.svg 
Here's what you would get if you use the files provided above (click to see
the original SVG version):