Cascading Stylesheets For Mapnik

The examples here demonstrate some of the basic usage of Mapnik cascading style sheets. Most of the syntax has been borrowed wholesale from CSS, so it should be familiar to web designers.

The examples show:

  1. Basic fill and stroke styles.
  2. Text, ID selectors.
  3. Images, class selector, projections.
  4. Externally-linked files, expanded class selectors.

The complete library is available from the mapnik-utils Google Code project.

Example 1

We start with a basic set of styles: a background color for the whole map, and an outline and fill for each country in the world borders data set.

Input example1.mml

There are two blocks in the Stylesheet portion of the file. One will define the map background color, the other will define line and polygon symbolizers for the world borders layer.

<?xml version="1.0" encoding="utf-8"?>
<Map srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
    <Stylesheet><![CDATA[
        Map
        {
            map-bgcolor: #69f;
        }

        Layer
        {
            line-width: 1;
            line-color: #696;
            polygon-fill: #6f9;
        }
    ]]></Stylesheet>
    <Layer srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Output XML

Converting example1.mml to output.xml looks like this:
python compile.py example1.mml > output.xml

<Map bgcolor="#6699ff" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
    <Style name="poly style 1">
        <Rule><PolygonSymbolizer><CssParameter name="fill">#66ff99</CssParameter></PolygonSymbolizer></Rule>
    </Style>
    <Style name="line style 2">
        <Rule><LineSymbolizer><CssParameter name="stroke">#669966</CssParameter><CssParameter name="stroke-width">1</CssParameter></LineSymbolizer></Rule>
    </Style>
    <Layer name="layer 3" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on">
        <StyleName>poly style 1</StyleName>
        <StyleName>line style 2</StyleName>
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Rendered Image

Example 2

Here we've added a separate block that labels each country. The selector for the block is Layer NAME, where NAME is the field from the data set to use as a label source. Note how it appears as name="NAME" in the output XML below.

We've also added an id attribute to the Layer, and used it in place of Layer for two of the selectors. The first block has been changed to a wildcard, *.

Input example2.mml

<?xml version="1.0" encoding="utf-8"?>
<Map srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
    <Stylesheet><![CDATA[
        *
        {
            map-bgcolor: #69f;
        }

        #world-borders
        {
            line-width: 1;
            line-color: #696;
            polygon-fill: #6f9;
        }

        #world-borders NAME
        {
            text-face-name: "DejaVu Sans Book";
            text-size: 10;
            text-fill: #000;
            text-halo-fill: #9ff;
            text-halo-radius: 2;
            text-placement: point;
            text-wrap-width: 50;
            text-avoid-edges: true;
        }
    ]]></Stylesheet>
    <Layer id="world-borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Output XML

<Map bgcolor="#6699ff" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
    <Style name="poly style 1">
        <Rule><PolygonSymbolizer><CssParameter name="fill">#66ff99</CssParameter></PolygonSymbolizer></Rule>
    </Style>
    <Style name="line style 2">
        <Rule><LineSymbolizer><CssParameter name="stroke">#669966</CssParameter><CssParameter name="stroke-width">1</CssParameter></LineSymbolizer></Rule>
    </Style>
    <Style name="text style 3 (NAME)">
        <Rule><TextSymbolizer avoid_edges="true" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="NAME" placement="point" size="10" wrap_width="50" /></Rule>
    </Style>
    <Layer name="layer 4" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on">
        <StyleName>poly style 1</StyleName>
        <StyleName>line style 2</StyleName>
        <StyleName>text style 3 (NAME)</StyleName>
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Rendered Image

Example 3

It's possible to refer to external images for use as point markers, such as this use of purple-point.png to accompany each country label. The point is found relative to example3.mml, and is referred to by an absolute path in the output XML. Although Mapnik can only accept PNG and TIFF images, we use PIL to convert all images to PNG's and save them in a temporary location for Mapnik's use. All the text has been offset by 10 pixels to make room.

The ID selectors from above have been replaced by class selectors, referring to the class="world-borders" attribute of the Layer. These work just like you'd expect from CSS. It is expected that a document has just one example of each ID, but potentially many mixed examples of each class.

We've also switched to a mercator projection.

Input example3.mml

<?xml version="1.0" encoding="utf-8"?>
<Map srs="+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">
    <Stylesheet><![CDATA[
        *
        {
            map-bgcolor: #69f;
        }

        .world-borders
        {
            line-width: 1;
            line-color: #696;
            polygon-fill: #6f9;
        }

        .world-borders NAME
        {
            text-face-name: "DejaVu Sans Book";
            text-size: 10;
            text-fill: #000;
            text-halo-fill: #9ff;
            text-halo-radius: 2;
            text-placement: point;
            text-wrap-width: 50;
            text-avoid-edges: true;

            point-file: url("purple-point.png");
            text-dy: 10;
        }
    ]]></Stylesheet>
    <Layer class="world-borders" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Output XML

<Map bgcolor="#6699ff" srs="+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">
    <Style name="poly style 1">
        <Rule><PolygonSymbolizer><CssParameter name="fill">#66ff99</CssParameter></PolygonSymbolizer></Rule>
    </Style>
    <Style name="line style 2">
        <Rule><LineSymbolizer><CssParameter name="stroke">#669966</CssParameter><CssParameter name="stroke-width">1</CssParameter></LineSymbolizer></Rule>
    </Style>
    <Style name="text style 3 (NAME)">
        <Rule><TextSymbolizer avoid_edges="true" dy="10" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="NAME" placement="point" size="10" wrap_width="50" /></Rule>
    </Style>
    <Style name="point style 4">
        <Rule><PointSymbolizer file="/tmp/cascadenik-point-JqnnpU.png" height="8" type="png" width="8" /></Rule>
    </Style>
    <Layer name="layer 5" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on">
        <StyleName>poly style 1</StyleName>
        <StyleName>line style 2</StyleName>
        <StyleName>text style 3 (NAME)</StyleName>
        <StyleName>point style 4</StyleName>
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Rendered Image

Example 4

Since the stylesheet has started to grow a little large, we've moved it off to a separate file, example4.mss. We've introduced a pattern background for each country pulled from a remote server by URL.

The selector syntax has been extended in two ways.

First, note the scale selection attribute selector syntax, e.g. [zoom>10]. The use of the zoom variable as a shorthand for MinScaleDenominator and MaxScaleDenominator is only meaningful when we use this exact projection SRS, which matches that used by OpenStreetMap, Google Maps, Virtual Earth, and others. If we were using some other projection but wanted to use Mapnik's scale support, we'd use something like [scale-denominator>=400000] instead.

Second, we see how multiple class names can be give to a single Layer, and how multiple matches can be used in selectors. In these examples there's only a single defined layer, but the .some-other-class line in example4.mss shows how it's possible to use single blocks of rules to affect numerous layers concurrently.

Input example4.mml

<?xml version="1.0" encoding="utf-8"?>
<Map srs="+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">
    <Stylesheet src="example4.mss"/>
    <Layer class="world-borders countries" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs">
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

example4.mss

*
{
    map-bgcolor: #69f;
}

.world-borders, .some-other-class
{
    line-width: 0.5;
    line-color: #030;
    polygon-fill: #6f9;

    point-file: url("purple-point.png");
    pattern-file: url("http://www.inkycircus.com/jargon/images/grass_by_conformity.jpg");
}

.world-borders.countries[zoom>10] NAME
{
    text-face-name: "DejaVu Sans Book";
    text-size: 10;
    text-fill: #000;
    text-halo-fill: #9ff;
    text-halo-radius: 2;
    text-placement: point;
    text-wrap-width: 50;
    text-avoid-edges: true;
    text-dy: 10;
}

.world-borders.countries[zoom<=10] FIPS
{
    text-face-name: "DejaVu Sans Book";
    text-size: 10;
    text-fill: #000;
    text-halo-fill: #9ff;
    text-halo-radius: 2;
    text-placement: point;
    text-wrap-width: 50;
    text-avoid-edges: true;
    text-dy: 10;
}

Output XML

<Map bgcolor="#6699ff" srs="+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">
    <Style name="poly style 1">
        <Rule><PolygonSymbolizer><CssParameter name="fill">#66ff99</CssParameter></PolygonSymbolizer></Rule>
    </Style>
    <Style name="pattern style 2">
        <Rule><PolygonPatternSymbolizer file="/tmp/cascadenik-pattern-8q8uHI.png" height="352" type="png" width="470" /></Rule>
    </Style>
    <Style name="line style 3">
        <Rule><LineSymbolizer><CssParameter name="stroke">#003300</CssParameter><CssParameter name="stroke-width">0.5</CssParameter></LineSymbolizer></Rule>
    </Style>
    <Style name="text style 4 (FIPS)">
        <Rule><MinScaleDenominator>400000</MinScaleDenominator><TextSymbolizer avoid_edges="true" dy="10" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="FIPS" placement="point" size="10" wrap_width="50" /></Rule>
    </Style>
    <Style name="text style 5 (NAME)">
        <Rule><MaxScaleDenominator>399999</MaxScaleDenominator><TextSymbolizer avoid_edges="true" dy="10" face_name="DejaVu Sans Book" fill="#000000" halo_fill="#99ffff" halo_radius="2" name="NAME" placement="point" size="10" wrap_width="50" /></Rule>
    </Style>
    <Style name="point style 6">
        <Rule><PointSymbolizer file="/tmp/cascadenik-point-E-iU9U.png" height="8" type="png" width="8" /></Rule>
    </Style>
    <Layer name="layer 7" srs="+proj=latlong +ellps=WGS84 +datum=WGS84 +no_defs" status="on">
        <StyleName>poly style 1</StyleName>
        <StyleName>pattern style 2</StyleName>
        <StyleName>line style 3</StyleName>
        <StyleName>text style 4 (FIPS)</StyleName>
        <StyleName>text style 5 (NAME)</StyleName>
        <StyleName>point style 6</StyleName>
        <Datasource>
            <Parameter name="type">shape</Parameter>
            <Parameter name="file">world_borders</Parameter>
        </Datasource>
    </Layer>
</Map>

Rendered Image