060-text.png - towns, cities, roads and motorways marked, labelled with text
Placing text on a map can be a complicated process. Here we look at the basics, putting labels on our places and roads.
#!/usr/bin/python# Load the Python mapnik libraries.importmapnik# Create a new map.m=mapnik.Map(480,320)# Set the background colour.m.background=mapnik.Color('ghostwhite')# Create a stroke object for railways.stroke_rail=mapnik.Stroke()stroke_rail.color=mapnik.Color('black')stroke_rail.dasharray=[(6,2)]stroke_rail.width=2# Create point, line and text symbolizers.point_symbolizer_city=mapnik.PointSymbolizer(mapnik.PathExpression('circle_red_16x16.png'))point_symbolizer_town=mapnik.PointSymbolizer(mapnik.PathExpression('circle_red_8x8.png'))line_symbolizer_rail=mapnik.LineSymbolizer(stroke_rail)line_symbolizer_road=mapnik.LineSymbolizer()line_symbolizer_mainroad=mapnik.LineSymbolizer(mapnik.Color('black'),2)line_symbolizer_motorway=mapnik.LineSymbolizer(mapnik.Color('lightblue'),4)text_symbolizer_roads=mapnik.TextSymbolizer()text_symbolizer_roads.name='[name]'text_symbolizer_roads.face_name='DejaVu Sans Book'text_symbolizer_roads.halo_radius=2text_symbolizer_roads.label_placement=mapnik.label_placement.linetext_symbolizer_towns=mapnik.TextSymbolizer()text_symbolizer_towns.name='[name]'text_symbolizer_towns.face_name='DejaVu Sans Book'text_symbolizer_towns.halo_radius=3text_symbolizer_towns.displacement=(0,7)text_symbolizer_cities=mapnik.TextSymbolizer()text_symbolizer_cities.name='[name]'text_symbolizer_cities.face_name='DejaVu Sans Book'text_symbolizer_cities.halo_radius=3text_symbolizer_cities.displacement=(0,11)text_symbolizer_cities.fill=mapnik.Color('red')text_symbolizer_cities.text_size=12# Create new rules and add the symbolizers.r_city=mapnik.Rule()r_city.symbols.append(point_symbolizer_city)r_city.symbols.append(text_symbolizer_cities)r_city.filter=mapnik.Filter('[type] = "city"')r_town=mapnik.Rule()r_town.symbols.append(point_symbolizer_town)r_town.symbols.append(text_symbolizer_towns)r_town.filter=mapnik.Filter('[type] = "town"')r_rail=mapnik.Rule()r_rail.symbols.append(line_symbolizer_rail)r_rail.filter=mapnik.Filter('[type] = "rail"')r_road=mapnik.Rule()r_road.symbols.append(line_symbolizer_road)r_road.symbols.append(text_symbolizer_roads)r_road.filter=mapnik.Filter('[type] = "road"')r_mainroad=mapnik.Rule()r_mainroad.symbols.append(line_symbolizer_mainroad)r_mainroad.symbols.append(text_symbolizer_roads)r_mainroad.filter=mapnik.Filter('[type] = "mainroad"')r_motorway=mapnik.Rule()r_motorway.symbols.append(line_symbolizer_motorway)r_motorway.symbols.append(text_symbolizer_roads)r_motorway.filter=mapnik.Filter('[type] = "motorway"')# Create new styles and add the rules.s_point=mapnik.Style()s_point.rules.append(r_town)s_point.rules.append(r_city)s_line=mapnik.Style()s_line.rules.append(r_rail)s_line.rules.append(r_road)s_line.rules.append(r_mainroad)s_line.rules.append(r_motorway)# Add the styles to the map.m.append_style('point_style',s_point)m.append_style('line_style',s_line)# Specify our data sources.ds_point=mapnik.CSV(file='data-places.csv')ds_line=mapnik.CSV(file='data-roads.csv')# Create new layers for the map, add the data sources and styles to# those layers.l_point=mapnik.Layer('point_layer')l_point.datasource=ds_pointl_point.styles.append('point_style')l_line=mapnik.Layer('line_layer')l_line.datasource=ds_linel_line.styles.append('line_style')# Add the layers to the map. We want the points to appear in front of the# lines, so we add the line layer first.m.layers.append(l_line)m.layers.append(l_point)# Zoom to the part of the map we are interested in.m.zoom_to_box(mapnik.Box2d(0,0,480,320))# Save the map as a PNG image.mapnik.render_to_file(m,'060-text.png','png')
Detailed explanation of this program
In this example we use a new type of symbolizer: the TextSymbolizer. This places text at points or along lines. We create three symbolizers: one for towns, one for cities and one for roads. Creating and configuring the TextSymbolizer objects takes place between lines 26 and 44.
For each symbolizer, we set the name attribute. This specifies an expression which provides the text we want to place on the map. For our data, we want the text in the "name" field to be printed for the towns, cities and roads. The expression [name] produces the value of that field, in the same way that [type] represents the field "type" in our rule filters. These attributes are set at lines 27, 33 and 39.
The face_name attribute for the symbolizers specifies the name of the font to be used for the text. We choose "DejaVu Sans Book" which should already be installed on your system as part of the Mapnik dependencies. To list the fonts available, type:[1]
python -c "from mapnik import FontEngine as e;print '\n'.join(e.instance().face_names())"
The following fonts should be available on all systems:
DejaVu Sans Bold
DejaVu Sans Bold Oblique
DejaVu Sans Book
DejaVu Sans Condensed
DejaVu Sans Condensed Bold
DejaVu Sans Condensed Bold Oblique
DejaVu Sans Condensed Oblique
DejaVu Sans ExtraLight
DejaVu Sans Mono Bold
DejaVu Sans Mono Bold Oblique
DejaVu Sans Mono Book
DejaVu Sans Mono Oblique
DejaVu Sans Oblique
DejaVu Serif Bold
DejaVu Serif Bold Italic
DejaVu Serif Book
DejaVu Serif Condensed
DejaVu Serif Condensed Bold
DejaVu Serif Condensed Bold Italic
DejaVu Serif Condensed Italic
DejaVu Serif Italic
When putting text onto a map, it can sometimes be difficult to read if there are other map features nearby. By setting the halo_radius attribute, we can get the symbolizer to add a small ring of space around the text, visually separating it from anything else present. We set this to two pixels of space around the road names, and three pixels around the place names. We set these values at lines 29, 35 and 41. The effect can be seen in several places on the map, including at the end of the text "Bury St Edmunds" where a small amount of road is removed to ensure the text is readable.
The text_size attribute allows us to specify the size of the text. For our city labels, we want a slightly larger text size than normal, so we specify a height of twelve pixels (points?) at line 44. For the other labels, the default of ten pixels (points?) will be fine.
The fill attribute controls the way in which the text is painted. For our city labels, we choose the colour red at line 43. The default of black will be fine for the other labels.
By default, a TextSymbolizer places text centrally over a point (or the middle of a line). For our town and city labels, we want them moved slightly downwards so that they don't attempt to occupy the same space as the red circles. The displacement allows us to specify where the text is to be placed relative to the point in question. For cities, we set a displacement of (0, 11) at line 42, meaning that the text label will be moved 11 pixels lower than it would be otherwise. For towns, which have a smaller circle, we set a slightly smaller displacement of (0, 7) at line 36.
The label_placement attribute of the TextSymbolizer allows to to specify how a label is drawn on a line. By default, the label is placed at the midpoint of the line, oriented horizontally. We set the value to mapnik.label_placement.line at line 30 to tell Mapnik to orient the label along the road instead.
Looking at the output, we start to see some of the results of the way in items are added to the map. Mapnik keeps track of where it has placed items on the map, and will not attempt to place further items which overlap with those already placed.
From the previous example, we expect to see a large red circle at Cambridge, but it is missing on this image. This is because the label for the A14 is placed exactly halfway along the road, and by chance ends up just where we would expect the red circle to be. When Mapnik comes to put the red circle in its place, the A14 label is therefore obstructing it, so the circle doesn't get drawn. This is also the case for Newmarket and the label for the A11. We could, of course, arrange for the circles to be drawn first, ensuring that there is nothing in their way, but with our current setup, this would mean that the roads get drawn on top of the red circles, which is not what we want.
Similarly, a few of the towns have a small red circle, but no label, for the same reason: St Ives, St Neots, Bedford and Royston.
Although the halo around the Bury St Edmunds label prevents the text from colliding with the road, the halos for the road names sometimes appear less effective. In particular, the A1198 label is placed on top of the A428, making it difficult to read. Similarly, the A605 label conflicts with the railway.
These are the sorts of problems which Mapnik can largely be avoided by more in-depth definition of Mapnik styles and rules. However, what we have achieved so far is a pretty good start.
Save this program in a file called 060-text.py and run it by typing:
python 060-text.py
You should see no error messages, and you should see a new file in your working directory called 060-text.png. This is a new map image, and should be a light-coloured rectangle 480 pixels wide by 320 pixels high, with towns, cities and roads labelled with text, as shown above.