User:Moresby/Understanding Mapnik/CSS styling with Cascadenik
First published in 2010, Cascadenik was an early attempt to apply some of the concepts of Cascading Style Sheets (CSS) to Mapnik. A map design is specified using a CSS-type syntax, and Cascadenik converts this into the appropriate XML input for Mapnik. Development of Cascadenik ceased in March 2013, although the ideas behind it fundamentally influenced the design of CartoCSS.
For example, the following Cascadenik input files provides a pretty good version of our example map. This is 260-text.mml
:
<?xml version='1.0'?>
<Map srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
<Stylesheet src='260-text.mss'/>
<Layer name='line_layer' class='road_lines road_labels'>
<Datasource>
<Parameter name='file'>data-roads.csv</Parameter>
<Parameter name='type'>csv</Parameter>
</Datasource>
</Layer>
<Layer name='point_layer' class='town_points town_labels city_points city_labels'>
<Datasource>
<Parameter name='file'>data-places.csv</Parameter>
<Parameter name='type'>csv</Parameter>
</Datasource>
</Layer>
</Map>
- This is an XML file, and is very similar to the XML files we have already seen as input to Mapnik. The main difference is that the several
<Style>
elements have been replaced by a single<Stylesheet>
element. This points to a separate file containing the style information for the map (see below). - At lines four and ten, the
<Layer>
elements now have aclass
attribute. Very similar to how HTML elements are assigned CSS classes, this tells Cascadenik which styles apply to which layers. - The <Map> element has an attribute we haven't seen before:
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
. This is a collection of parameters which describe the data projection: how points on the earth's surface are represented on the map. We've not needed to specify this before, as the defaults for Mapnik were sufficient to plot our basic coordinates, but Cascadenik's defaults are different, so we need to specify what Mapnik has been using for previous maps.- The projection specified here treats our
x
andy
values as degrees longitude and latitude, using the WGS84 geodetic datum. - The default projection provided by Cascadenik is
+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
, which is the projection used by Google and OpenStreetMap for their maps.
- The projection specified here treats our
- Knowing what projection is being used becomes very important when combining data from several sources. For our purposes, we can afford not to worry about it for now.
And this is 260-text.mss
:
/* Set background colour to 'ghostwhite'. */
Map {
map-bgcolor: #f8f8ff;
}
/* Specify how to draw the road labels. */
.road_labels [type != 'rail'] name {
text-face-name: 'DejaVu Sans Book';
text-size: 10;
text-fill: #000;
text-placement: line;
text-halo-radius: 2;
}
/* For rail, we have a wide dashed line. */
.road_lines [type = 'rail'] {
line-color: #000;
line-width: 2;
line-dasharray: 6, 2;
}
/* Roads are narrow, black lines. */
.road_lines [type = 'road'] {
line-color: #000;
line-width: 1;
}
/* Main roads are wider, black lines. */
.road_lines [type = 'mainroad'] {
line-color: #000;
line-width: 2;
}
/* Main roads are wide, light blue lines. */
.road_lines [type = 'motorway'] {
line-color: #add8e6;
line-width: 4;
}
/* Towns are marked with a small circle. */
.town_points [type = 'town'] {
point-file: url('circle_red_8x8.png');
}
/* Towns names are drawn in black. */
.town_labels [type = 'town'] name {
text-face-name: 'DejaVu Sans Book';
text-size: 10;
text-fill: #000;
text-halo-radius: 3;
text-dy: 7;
}
/* Cities are marked with a larger circle. */
.city_points [type = 'city'] {
point-file: url('circle_red_16x16.png');
}
/* City names are drawn in red. */
.city_labels [type = 'city'] name {
text-face-name: 'DejaVu Sans Book';
text-size: 12;
text-fill: #f00;
text-halo-radius: 3;
text-dy: 11;
}
- Lines 16–39 specify how various lines should be represented on the map. The syntax is very similar to Cascading Style Sheets (CSS), including a CSS2.1 attribute selector, an expression in square brackets such as
[type = 'rail']
. This expression corresponds to the Mapnik filter and allows us to specify styles which apply to only those objects with particular values for their associated data. The propertiesline-color
andline-width
refer to the colour and width of the desired line, and both need to be specified (or inherited) in a style for a line to be drawn. The propertyline-dasharray
is optional, and specifies the Mapnik dasharray for the line. - Lines 42–44 and 57–59 specify that images should be used to represent towns and cities, with the
point-file
properties giving the names of the image files to use. - Lines 47–54 and 62–69 control the labelling of the towns and cities: each of the properties
text-face-name
text-size
text-fill
has to be specified (or inherited) for Cascadenik to generate the code to produce a Mapnik TextSymbolizer. The text to be placed on the map is the value of the field specified by the additional label in the selector (lines 47 and 62), in this casename
. The required halo radius and y-offset are also specified in these sections. - Lines 7–14 similarly specify that road names should written along the line for all road types except
rail
. - In this example, the styles are defined with class selectors and referred to in the MML file above using lists of applicable class names, such as
class='road_lines road_labels'
. Alternative CSS-style approaches can be used, including the element name (see line 2) or element ID values. - There appears to be a significant aspect missing of Cascadenik's implementation of CSS styling. A different approach to this same map might be to make more use of CSS's cascading approach, attempting to specify road labelling in general, and then specifying how it differs in the case of railways. We could specify this by removing the attribute selector in line 7 and then adding an additional section to style just the railways (
.road_labels [type = 'rail'] name { … }
). However, for railways we wish to have no labelling, and Cascadenik seems not to provide a mechanism for turning off an inherited property, along the lines of HTML/CSS'sborder: none;
option. This affects our ability to take advantage of all the promise of a CSS approach.
Save the first of these files as 260-text.mml
and the second as 260-text.mss
and run:
python cascadenik-compile.py 260-text.mml 260-text.xml
This will create the following Mapnik XML input file 260-text.xml
:
<Map background-color="rgb(248,248,255)" srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
<Style filter-mode="first" name="line style 3">
<Rule name="rule 1">
<Filter>([type]='mainroad')</Filter>
<LineSymbolizer stroke-width="2" />
</Rule>
<Rule name="rule 2">
<Filter>([type]='motorway')</Filter>
<LineSymbolizer stroke="rgb(173,216,230)" stroke-width="4" />
</Rule>
<Rule name="rule 3">
<Filter>([type]='rail')</Filter>
<LineSymbolizer stroke-dasharray="6, 2" stroke-width="2" />
</Rule>
<Rule name="rule 4">
<Filter>([type]='road')</Filter>
<LineSymbolizer />
</Rule>
</Style>
<Style filter-mode="first" name="point style 15">
<Rule name="rule 8">
<Filter>([type]='city')</Filter>
<PointSymbolizer file="/home/moresby/work/circle_red_16x16.png" />
</Rule>
<Rule name="rule 9">
<Filter>([type]='town')</Filter>
<PointSymbolizer file="/home/moresby/work/circle_red_8x8.png" />
</Rule>
</Style>
<Style filter-mode="first" name="text style 14 (name)">
<Rule name="rule 6">
<Filter>([type]='city')</Filter>
<TextSymbolizer clip="false" dy="11" face-name="DejaVu Sans Book" fill="rgb(255,0,0)" halo-radius="3" size="12">[name]</TextSymbolizer>
</Rule>
<Rule name="rule 7">
<Filter>([type]='town')</Filter>
<TextSymbolizer clip="false" dy="7" face-name="DejaVu Sans Book" halo-radius="3">[name]</TextSymbolizer>
</Rule>
</Style>
<Style filter-mode="first" name="text style 6 (name)">
<Rule name="rule 5">
<Filter>not (([type]='rail'))</Filter>
<TextSymbolizer clip="false" face-name="DejaVu Sans Book" halo-radius="2" placement="line">[name]</TextSymbolizer>
</Rule>
</Style>
<Layer name="layer 8" srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
<StyleName>line style 3</StyleName>
<StyleName>text style 6 (name)</StyleName>
<Datasource>
<Parameter name="file">/home/moresby/work/data-roads.csv</Parameter>
<Parameter name="type">csv</Parameter>
</Datasource>
</Layer>
<Layer name="layer 16" srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
<StyleName>text style 14 (name)</StyleName>
<StyleName>point style 15</StyleName>
<Datasource>
<Parameter name="file">/home/moresby/work/data-places.csv</Parameter>
<Parameter name="type">csv</Parameter>
</Datasource>
</Layer>
</Map>
Now use Mapnik to turn this into a map:
python generate-map.py 260-text.xml
You should see no error messages, and you should see a new file in your working directory called 260-text.png. This should be almost identical to the previous map we produced, as shown above.