User:Moresby/Understanding Mapnik/Adding text to the map
Jump to navigation
Jump to search
Understanding Mapnik
A Mapnik tutorial
Starting with Python
Using XML and CSS
CartoCSS and PostGIS
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.
import mapnik
# 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 = 2
text_symbolizer_roads.label_placement = mapnik.label_placement.line
text_symbolizer_towns = mapnik.TextSymbolizer()
text_symbolizer_towns.name = '[name]'
text_symbolizer_towns.face_name = 'DejaVu Sans Book'
text_symbolizer_towns.halo_radius = 3
text_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 = 3
text_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_point
l_point.styles.append('point_style')
l_line = mapnik.Layer('line_layer')
l_line.datasource = ds_line
l_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 tomapnik.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.