From OpenStreetMap Wiki
Jump to: navigation, search

This page describes how to install the software you need for creating a map like (see also openptmap Wiki page). The advanced approach, so-called diff-based rendering is presently being tested on a second server:



A weak CPU, e.g. Intel Atom or a virtual Internet server, will suffice if you limit the geographical region (e.g. France, Germany, Australia). 1 GB RAM is recommended, but 768 MB will be enough.

Operating System

We assume that you use Ubuntu >=8.04 as operating system. All of the following steps have been tested with version 10.04, most have been tested with 8.04 too. Meanwhile, short term versions >=10.10 are available. There, the procedure has not been tested yet, but it should work as well.

Prepare your System

This chapter describes all steps which are to be done once. After having run through this work, you will only need to update the OSM data for your map from time to time.

Create a User

For all openptmap purposes you can use your usual user account if you like. However, it is recommended to create a separate user for that. In this example, we decide to create a user account with the name pt. Please log in with a user with administrator rights and enter the following commands:

sudo adduser pt
sudo usermod -aG admin pt

You will have noticed that we gave admin rights to user pt. These rights should be removed after the whole installation process has been completed: sudo deluser pt admin Now logoff, then login with the new user name pt.

Debian Packages

You must install all packages necessary to run your own web server. Additionally, you will need some other packages. Ensure the installation of all necessary packages by performing these commands:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install apache2 php5 php5-pgsql libapache2-mod-php5 subversion autoconf autoconf2.13 unzip
sudo apt-get install automake build-essential libxml2-dev libgeos-dev libbz2-dev proj libtool
# for Ubuntu <=9.10:
  sudo apt-get install postgresql-8.3-postgis postgresql-contrib-8.3 postgresql-server-dev-8.3
# for Ubuntu >=10.04:
  sudo apt-get install postgresql-8.4-postgis postgresql-contrib-8.4 postgresql-server-dev-8.4

PostgreSQL Database

The GIS database must be intitialized. Please follow these steps:

sudo -u postgres -i -H
createuser -SdR ptuser
createdb -E UTF8 -O ptuser ptgis
createlang plpgsql ptgis
# for Ubuntu <=9.10:
  psql -d ptgis -f /usr/share/postgresql-8.3-postgis/lwpostgis.sql
# for Ubuntu >=10.04:
  psql -d ptgis -f /usr/share/postgresql/8.4/contrib/postgis.sql
# for Ubuntu 11.04:
  psql -d ptgis -f /usr/share/postgresql/8.4/contrib/postgis-1.5/postgis.sql
psql ptgis -c "ALTER TABLE geometry_columns OWNER TO ptuser"
psql ptgis -c "ALTER TABLE spatial_ref_sys OWNER TO ptuser"

To enable easy database login by user ptuser you must change some lines in one of the database configuration files. In case you are running Ubuntu with a graphical interface, you could a more comfortable editor, e.g. gedit instead of nano.

# for Ubuntu <=9.10:
  sudo nano /etc/postgresql/8.3/main/pg_hba.conf
# for Ubuntu >=10.04:
  sudo nano /etc/postgresql/8.4/main/pg_hba.conf

Near to the bottom of the file you will find these lines:

local   all         all                               ident
host    all         all          md5
host    all         all         ::1/128               md5

Change the words ident and md5 each to trust and close the editor (for nano: Ctrl-O, Enter, Ctrl-X). Now reload the database configuration:

# for Ubuntu <=9.10:
  sudo /etc/init.d/postgresql-8.3 reload
# for Ubuntu >=10.04:
  sudo /etc/init.d/postgresql-8.4 reload

For a short test, login to the database by using the previously created database user ptuser:

psql ptgis ptuser

Type \d to see a list with the two tables which we have created some steps above (geometry_columns and spatial_ref_sys). Then logout with \q

If you encounter any problems, you may find a solution here: PostGIS.

Choose a Directory

Our example directory for downloading OSM data and generating the data base contents will be the home directory of the user pt, "/home/pt", which can be abbreviated as "~" if being logged in with this user.

If you choose a different directory, please create it and grant read and write access rights to the user pt. You also will have to replace all cd /home/pt commands in the following examples with cd and the full path to your alternative directory.

Tool osm2pgsql

We will need the tool osm2pgsql to write OSM data into the database. Although provided by Ubuntu community, it's strongly recommended to build osm2pgsql from source, because we rely on getting the newest version. Please follow these steps:

cd /home/pt
svn export
cd osm2pgsql

We need to copy and extend the file "" which should be located in the directory /home/pt/osm2pgsql. You can do this with an editor or by executing these commands:

cd /home/pt
cp osm2pgsql/
cat <<eof >>

# extensions for openptmap:
node,way line text linear
node,way ref_name text linear
node,way uic_ref text linear
node,way public_transport text linear
node,way train text linear
node,way wheelchair text linear
node,way website text linear

Now the database needs to be initialized for Spherical Mercator projection:

cd /home/pt
psql ptgis ptuser -f osm2pgsql/900913.sql

If you encounter any problems, you may find a solution here: osm2pgsql.

Tools osmconvert and osmfilter

Both tools are used to accelerate the PostgreSQL import process. If we filter out all information we want to use and drop everything else, osm2pgsql will run faster. On top of this, database queries will be faster too, so the rendering process will be accelerated as well. You can download the programs as binary, however, it is recommended to download and compile the source code because the binary may be out of date. To do this – downloading and compiling – from the command line, enter these commands:

cd /home/pt
wget -O - |cc -x c - -lz -o osmconvert
wget -O - |cc -x c - -o osmfilter

For further details on both tools refer to osmconvert and osmfilter.

Mapnik Renderer

First, check if your package sources offer Mapnik version 0.7.1:

apt-cache show python-mapnik

If yes, install Mapnik package:

sudo apt-get install python-mapnik

If there is no Mapnik package available or it has a lower version number, you must install Mapnik from source. There is an installation guide which will help you: Ubuntu Installation Guide for Mapnik

For example the procedure for Ubuntu 8.04:

cd /home/pt
sudo apt-get purge python-mapnik
sudo apt-get autoremove
# boost dependencies
sudo apt-get install binutils cpp-3.3 g++-3.3 gcc-3.3 gcc-3.3-base libboost-dev libboost-filesystem-dev libboost-filesystem1.34.1 libboost-iostreams-dev libboost-iostreams1.34.1 libboost-program-options-dev libboost-program-options1.34.1 libboost-python-dev libboost-python1.34.1 libboost-regex-dev libboost-regex1.34.1 libboost-serialization-dev libboost-serialization1.34.1 libboost-thread-dev libboost-thread1.34.1 libicu-dev libicu38 libstdc++5 libstdc++5-3.3-dev python2.5-dev 
# remaining required dependencies
sudo apt-get install libfreetype6 libfreetype6-dev libjpeg62 libjpeg62-dev libltdl3 libltdl3-dev libpng12-0 libpng12-dev libtiff4 libtiff4-dev libtiffxx0c2 python-imaging python-imaging-dbg proj 
# optional Cairo Renderer dependencies
sudo apt-get install libcairo2 libcairo2-dev python-cairo python-cairo-dev libcairomm-1.0-1 libcairomm-1.0-dev libglib2.0-0 libpixman-1-0 libpixman-1-dev libpthread-stubs0 libpthread-stubs0-dev ttf-dejavu ttf-dejavu-core ttf-dejavu-extra
# all Optional GIS utilities
sudo apt-get install libgdal-dev python2.5-gdal postgresql-8.3-postgis postgresql-8.3 postgresql-server-dev-8.3 postgresql-contrib-8.3
# WMS Dependencies
sudo apt-get install libxslt1.1 libxslt1-dev libxml2-dev libxml2
 #sudo apt-get install python-pip
 #easy_install jonpy
 #easy_install lxml
# build Mapnik
svn co mapnik_src
cd mapnik_src
python scons/
sudo python scons/ install

For example the procedure for Ubuntu 10.04:

cd /home/pt
sudo apt-get purge python-mapnik
sudo apt-get autoremove
sudo apt-get install g++ cpp \
libboost1.40-dev libboost-filesystem1.40-dev \
libboost-iostreams1.40-dev libboost-program-options1.40-dev \
libboost-python1.40-dev libboost-regex1.40-dev \
libboost-thread1.40-dev \
python-dev libxml2 libxml2-dev \
libfreetype6 libfreetype6-dev \
libjpeg62 libjpeg62-dev \
libltdl7 libltdl-dev \
libpng12-0 libpng12-dev \
libgeotiff-dev libtiff4 libtiff4-dev libtiffxx0c2 \
libcairo2 libcairo2-dev python-cairo python-cairo-dev \
libcairomm-1.0-1 libcairomm-1.0-dev \
ttf-unifont ttf-dejavu ttf-dejavu-core ttf-dejavu-extra \
subversion build-essential python-nose libcurl4-gnutls-dev
sudo apt-get install libsigc++-dev libsigc++0c2 libsigx-2.0-2 libsigx-2.0-dev
sudo apt-get install libgdal1-dev python-gdal \
postgresql-8.4 postgresql-server-dev-8.4 postgresql-contrib-8.4 \
postgresql-8.4-postgis libsqlite3-dev
svn co mapnik_src
cd mapnik_src
python scons/ configure INPUT_PLUGINS=all OPTIMIZATION=3 SYSTEM_FONTS=/usr/share/fonts/
python scons/
sudo python scons/ install

Be prepared that Mapnik may take an hour to be compiled. Of course, you can use Synaptic Packet Administration to accomplish this, but ensure to get at least Mapnik version 0.7.1.

Mapnik Tools

We already installed Mapnik. Now we should care about some additional files we will need in this context.

cd /home/pt
svn export
svn export mapnik-utils
cd mapnik-utils
sudo python install


Mapnik needs external shape-file data for coastlines at lower zoom levels; let's download them now. Afterwards we have to unpack the files and dissolve possible subdirectories. The whole procedure can be accomplished by one of the OSM scripts:

cd /home/pt
cd mapnik
rm *.tar.bz2 *.tgz *.zip

In case this did not work, go the manual way:

cd /home/pt
mkdir mapnik
mkdir mapnik/world_boundaries
cd mapnik/world_boundaries
rm ../world_boundaries/*
tar -xjf *.tar.bz2
tar -xzf *.tgz
bunzip2 *.bz2
unzip *.zip
rm *.tar.bz2 *.tgz
mv */* .
rmdir * 2>/dev/null

Mapnik Initialization

For Mapnik, some initializations are to be done. Afterwards, we can create our own Mapnik rendering styles. The file osm.xml will be used as a template for this. In this case, we must add some layers with style information about the representation of public transport routes.

cd /home/pt
cd mapnik
./ --host localhost --user ptuser --dbname ptgis --symbols ./symbols/ --world_boundaries ./world_boundaries/ --port '' --password ''
cp osm.xml mapnik_pt.xml
nano mapnik_pt.xml

Prepare the Website

The web server Apache will expect our web content at "/var/www". Note that you will need write-access rights in this directory. If these rights have not been set automatically during Apache installation, you may want to do this by hand:

sudo chown root:www-data /var/www
sudo chmod g+rwx /var/www
sudo usermod -aG www-data pt

To get these changes work, you will need to relogin. If you had chosen another user account, not pt, than please change the last command accordingly.

Having done this, change to that directory and download all necessary files.

cd /var/www
mkdir img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img
wget -P img

Now delete the existing index file with sudo rm index.html and create a new one with nano index.html and insert this contents:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="de">
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
  <title>Public Transport Lines</title>
  <link rel="stylesheet" href="style.css" type="text/css">
  <script src="OpenLayers.js" type="text/javascript"></script>
  <script src="OpenStreetMap.js" type="text/javascript"></script>
  <script type="text/javascript">
    // Start position for the map (hardcoded here for simplicity)
    var lon=11;
    var lat=51;
    var zoom=7;
    var map; //complex object of type OpenLayers.Map
    OpenLayers.Protocol.HTTPex= new OpenLayers.Class(OpenLayers.Protocol.HTTP, {
      read: function(options) {,arguments);
        options= OpenLayers.Util.applyDefaults(options,this.options);
        options.params= OpenLayers.Util.applyDefaults(
        options.params.resolution= map.getResolution();
        options.params.zoom= map.getZoom();
        if(options.filter) {
          options.params= this.filterToParams(
            options.filter, options.params);
        var readWithPOST= (options.readWithPOST!==undefined)?
          options.readWithPOST: this.readWithPOST;
        var resp= new OpenLayers.Protocol.Response({requestType: "read"});
        if(readWithPOST) {
          resp.priv= OpenLayers.Request.POST({
            url: options.url,
            callback: this.createCallback(this.handleRead,resp,options),
            data: OpenLayers.Util.getParameterString(options.params),
            headers: {"Content-Type": "application/x-www-form-urlencoded"}
        } else {
          resp.priv= OpenLayers.Request.GET({
            url: options.url,
            callback: this.createCallback(this.handleRead,resp,options),
            params: options.params,
            headers: options.headers
        return resp;
      CLASS_NAME: "OpenLayers.Protocol.HTTPex"

    var popwin= null;

    function onPopupClose(evt) {
      popwin= null;

    function init() {
      var args= OpenLayers.Util.getParameters(); ///

      OpenLayers.Util.onImageLoadError= function() {
        this.src= "emptytile.png"; };

      map= new OpenLayers.Map("map", {
          new OpenLayers.Control.Navigation(),
          new OpenLayers.Control.PanZoomBar(),
          new OpenLayers.Control.LayerSwitcher(), ///
          new OpenLayers.Control.Permalink(),
          new OpenLayers.Control.ScaleLine(),
          new OpenLayers.Control.Permalink('permalink'),
          new OpenLayers.Control.MousePosition(),                    
          new OpenLayers.Control.Attribution()],
        maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),
        maxResolution: 156543.0399,
        numZoomLevels: 17,
        units: 'm',
        projection: new OpenLayers.Projection("EPSG:900913"),
        displayProjection: new OpenLayers.Projection("EPSG:4326"),
        restrictedExtent: new OpenLayers.Bounds(550000,5700000,2000000,7450000)
        } );

    map.addLayer(new OpenLayers.Layer.OSM.Mapnik("Landkarte",
      { maxZoomLevel: 17, numZoomLevels: 18 }));
    map.addLayer(new OpenLayers.Layer.OSM.Mapnik("Landkarte, blass",
      { maxZoomLevel: 17, numZoomLevels: 18, opacity: 0.5 }));
    map.addLayer(new OpenLayers.Layer.OSM.CycleMap("Radkarte",
      { maxZoomLevel: 17, numZoomLevels: 18 }));

    map.addLayer(new OpenLayers.Layer.OSM.CycleMap("Radkarte, blass",
      { maxZoomLevel: 17, numZoomLevels: 18, opacity: 0.5 }));

    map.addLayer(new OpenLayers.Layer.OSM("Kein Hintergrund","img/blank.png",
      { maxZoomLevel: 17, numZoomLevels: 18 }));

    map.addLayer(new OpenLayers.Layer.OSM("&Ouml;V-Linien","tiles/${z}/${x}/${y}.png",
      { maxZoomLevel: 17, numZoomLevels: 18, alpha: true, isBaseLayer: false }));

    var lay= new OpenLayers.Layer.Vector("Rollstuhleignung", {
      visibility: false,
      strategies: [new OpenLayers.Strategy.BBOX({resFactor: 1.1})],
      protocol: new OpenLayers.Protocol.HTTPex({
        url: "ptfeatures.php?f=w",
        format: new OpenLayers.Format.Text()
      maxZoomLevel: 17, numZoomLevels: 18,
      transparent: true, isBaseLayer:false, maxResolution : 4.8  // i.e. zoom 15..18

    lay= new OpenLayers.Layer.Vector("Fahrplan per Klick", {
      //visibility: false,
      strategies: [new OpenLayers.Strategy.BBOX({resFactor: 1.1})],
      protocol: new OpenLayers.Protocol.HTTPex({
        url: "ptfeatures.php?f=a",
        format: new OpenLayers.Format.Text()
      maxZoomLevel: 17, numZoomLevels: 18,
      transparent: true, isBaseLayer:false, maxResolution : 4.8  // i.e. zoom 15..18

      var lonLat= new OpenLayers.LonLat(lon, lat).transform(new OpenLayers.Projection("EPSG:4326"),

<body onload="init();">
<div style="width:100%; height:100%;" id="map"></div><br>
<div style="border:1px solid black; padding:10px;">
(Enter some map description here...)

Further information can be found here.

Create the Map

This chapter will show you how to create or update the tiles for your map. Tiles are small bitmaps which will be assembled to a whole map by the map browser. We assume that all tasks of the previous chapter have been completed successfully.

The process of map creation involves two steps – filling the database and rendering the map tiles.

Fill the Database

All information we need to create the map tiles must be written into our database. To do this, we need to develop a script which performs several tasks, step by step. Please do not try to create and run the whole script from scratch. It is better to run-through the process step by step, entering every single command at the command line terminal. That makes it much easier to find errors.

Get the Planet File

It is not recommended to use the file of the entire planet. Please choose the file of an area you are interested in, e.g. Germany. The first task of our script will be to download this file and to store it using .o5m file format.

cd /home/pt
wget -O - |./osmconvert - --out-o5m >a.o5m

Filter the Planet File

We need to do a hierarchic filtering because there will be nodes in ways, ways in relations and relations in other relations. For performance reasons a pre-filtered file will be used for interrelational filtering as there is no need for considering nodes or ways in the first filtering stages. In this example we decide to filter public-transport specific information because we want to create a public transport map.

./osmfilter a.o5m --keep="amenity=bus_station highway=bus_stop =platform public_transport=platform railway=station =stop =tram_stop =platform =rail route=rail =train =light_rail =subway =tram route=bus =trolleybus =ferry =railway =funicular line=rail =light_rail =subway =tram =bus =trolleybus =ferry =railway type=public_transport" --drop="railway=platform" >gis.osm

Some seconds later – dependant of the size of the chosen area – we will get the file gis.osm, containing only that information we need.

Note: We are using .o5m file format, because this will accelerate the filter process significantly. See also osmconvert and Daily update an OSM XML file.

Transfer the Data to Postgres Database

Now we can transfer the OSM data from the file gis.osm into the Postgres database. The program osm2pgsql will do this job:

osm2pgsql/osm2pgsql -s -C 700 -d ptgis -U ptuser -S gis.osm

Because the .osm file had been filtered in the previous step, this transfer should take only a few minutes.

Render the Tiles

At first, we should test if the renderer works as expected. Thereby, the nik2img program will help us. Let's pick an area for which we have already imported GIS data – in this example: Nürnberg, Germany – generate a test image and examine it with the viewer Eye of Gnome.

cd /home/pt
cd mapnik /home/pt/mapnik_pt.xml image.png -c 11 49.5 -z 10 -d 800 400 --no-open -f png256
eog image.png

If the test run was successful, the next step should be generating map tiles. For this purpose, we use the script First, the bounding box must be adjusted to our map area. Open the script with gedit and delete all the lines below # Start with an overview.

cd /home/pt
cd mapnik

Replace the deleted lines with these (do not change the 4 spaces indent):

    force = int(os.environ['F'])
    x1 = float(os.environ['X1'])
    y1 = float(os.environ['Y1'])
    x2 = float(os.environ['X2'])
    y2 = float(os.environ['Y2'])
    minZoom = int(os.environ['Z1'])
    maxZoom = int(os.environ['Z2'])
    bbox = (x1,y1,x2,y2)
    render_tiles(bbox, mapfile, tile_dir, minZoom, maxZoom)

Then go up and replace the definition of the function loop (it starts with def loop(self): and ends with self.q.task_done()). Replace the whole definition by these lines (do not change the indents):

    def loop(self):
        while True:
            #Fetch a tile from the queue and render it
            r= self.q.get()
            if r==None:
            (name,tile_uri,x,y,z)= r
                mtime= os.stat(tile_uri)[8]  # ST_MTIME
                mtime= 961063200  # June 2000
            if z<=12 or (mtime>=946681200 and mtime<=978303540) or force==1:
                    # low Zoom OR mtime between Jan 2000 and Dec 2000

Now start the script:

cd /home/pt
cd mapnik
F=1 X1=5.8 Y1=47.2 X2=15.1 Y2=55.0 Z1=5 Z2=15 MAPNIK_MAP_FILE="/home/pt/mapnik_pt.xml" MAPNIK_TILE_DIR="/var/www/tiles/" ./

Depending of the hardware, it may take several hours to render all the tiles. In case you want to abort the rendering, you have to use the Gnome System Monitor or the command kill and kill the process because Ctrl-C does not work here.

Test your new Map

Open a web browser and try to access your new website. If your browser is installed on the same computer as the Apache server, type localhost as URL.

Create the Map Automatically

Did all previous steps work without any errors? Then it's time to pack them into a single script:

cd /home/pt
chmod ug+x

That content could be like this:

cd /home/pt
mv gen.log gen.log_temp
tail -1000 gen.log_temp>gen.log
rm gen.log_temp
echo >>gen.log
date >>gen.log

# download section - start
rm a.o5m
wget -O - 2>/dev/null |./osmconvert --out-o5m >a.o5m
date >>gen.log
# download section - end
if [ -z a.osm.gz"" -o $(stat -L -c%s a.osm.gz) -lt 1000000 ]; then
  echo "OSM file download error" >>upd.log
  date >>gen.log
ls -lL a.osm.gz >>gen.log
date >>gen.log
./osmfilter a.o5m --keep="amenity=bus_station highway=bus_stop =platform public_transport=platform railway=station =stop =tram_stop =platform =rail route=rail =train =light_rail =subway =tram route=bus =trolleybus =ferry =railway =funicular line=rail =light_rail =subway =tram =bus =trolleybus =ferry =railway type=public_transport" --drop="railway=platform" >gis.osm
ls -lL gis.osm >>gen.log
date >>gen.log

echo Generating data base tables >>gen.log
osm2pgsql/osm2pgsql -s -C 700 -d ptgis -U ptuser -S gis.osm 2>/dev/null >/dev/null
date >>gen.log

echo Rendering the tiles >>gen.log
cd mapnik
F=1 X1=5.8 Y1=47.2 X2=15.1 Y2=55.0 Z1=5 Z2=15 MAPNIK_MAP_FILE="/home/pt/mapnik_pt.xml" MAPNIK_TILE_DIR="/var/www/tiles/" ./ 2>&1>/dev/null
cd ..
date >>gen.log

Weekly Map Data Update

To get the map data updated automatically every week, an additional Cron entry will be mandatory:

sudo echo -e "0 4 * * 7 root /usr/share/ptgen/ 2>&1>/dev/null \n" > /etc/cron.d/ptgen

Daily Map Data Update

Instead of downloading the whole regional .osm file, you can also use the mechanism for a daily update. After you have followed the description in that page, create a symbolic link to the automatically updated .o5m file (insert the right path):

cd /home/pt
ln -fs /insert_here_the_actual_path_to_osmupdate_directory/a.o5m a.o5m

If you use the script for automatic map creation from above, you will no longer need the script's download part. Hence, remove the 3 lines which are included by the # download section markings.

Using a Rendering Strategy

You can improve the rendering speed significantly if you render only that tiles which have been requested at least once within the last rendering period. The idea behind this strategy is that only those tiles which are frequently used must be kept up-to-date. To activate this strategy you need to change the F=1 to F=0 at the beginning of the line with the rendering command.

Now, in lower zoom levels only those tile files will be rendered which do not yet exist or have a file modification time which lies in the year 2000. Of course, we must ensure now that every tile which has been served by Apache, is being marked accordingly. Therefore we will have to run this command every day some minutes after midnight:

LANG=en_US.utf8; cat /var/log/apache2/access.log.1 /var/log/apache2/access.log 2>/dev/null |grep "$(date -d yesterday +"\[%d/%b/%Y")" |cut -d" " -s -f 7 |grep -e "/tiles/13/" -e "/tiles/14/" -e "/tiles/15/" -e "/tiles/16/" |sort -u |sed s"/^/\/var\/www/" |xargs -I '{}' touch -c -t200006151200 '{}'

With this single command, the last Apache log files are searched for tile request entries which have been recorded yesterday. The modification time of all these tile files is set to June 2000.

We should pack this command into script, let's name it, and add another Cron entry:

sudo echo -e "45 0 * * * root /usr/share/ptgen/ 2>&1>/dev/null \n" > /etc/cron.d/marktiles

Note that using incurs a significant performance penalty compared to the typical Tirex or renderd setups, as tirex and renderd will render "metatiles (8x8 tiles) in one go. Rendering a metatile takes on average 4 times as long as rendering a single tile, i.e. you achieve 16 times the tile throughput of - provided that the tiles you render are close to each other.

An Advanced Approach: Diff-based Rendering

In the previous chapter we updated map tiles which have been watched by at least one visitor of the map's website. This saves a lot of rendering effort but it is still far from being optimal.

Much more efficient would it be if we rendered only that tiles whose OSM data have been changed. With the right call, the program osm2pgsql will supply us with a list of these tiles. This is usually not very helpful for a thematic overlay map because 90% of the changed data will not affect the overlay image, but there is a way to get exactly the information we need: update the PostgreSQL database with OSM-Change files which have been retrieved from intensively filtered data.

Some Details to the Alternative Strategy

The idea is to compare the filtered data from before an update against the filtered data from after this update, and then to generate an OSM-Change file (.osc) on the basis of this difference.

This OSM-Change file should not bee very large because it is retrieved from filtered data, therefore the dirty_tiles list which is produced by osm2pgsql will not be long and the regular rendering process should not take days to conclude.

Getting a List of all Dirty Tiles

Speaking of the file dirty_tiles, the program osm2pgsql does not provide us with the names of all affected tiles because this list is stripped of all redundant information. If there is, for example, the tile 3/0/1 (zoom, x, y) in the list, the also affected tile 2/0/0 will be omitted. Furthermore, if all four subtiles – 3/0/0, 3/0/1, 3/1/0 and 3/1/1 – where affected, none of them would appear in the list, they would be replaced by the entry 2/0/0.

This is a very effective way to reduce the dirty-tiles list length but we still need a list of all effected tiles. For this reason, we decide to append one of osm2pgql's source files: "expire-tiles.c".

Omit Empty Tiles

Empty tiles on a transparent map layer are completely transparent graphics and not of any use for our map, they just waste disk space. Therefore it is better to refrain from saving them in the first place. This will be accomplished by a few additional lines in the Mapnik Python script.

OpenLayers must be informed about this intention also, otherwise it would post error messages upon missing tiles or trying to wait for them.

Schematic Diagram

Here is an overview of the programs and files we are going to use. This diagram is somewhat simplified; it has been created to show all main steps of the strategy in one picture.

                  | osmupdate |
      a.osm ----> | -B=p.poly | ----> b.osm
        |         +-----------+         |
        v               ^               v
+---------------+       |       +---------------+
|   osmfilter   |     p.poly    |   osmfilter   |
| --keep=route= |               | --keep=route= |
+---------------+               +---------------+
        |                               |
        v         +------------+        v
      fa.osm ---> | osmconvert | <--- fb.osm
                  |   --diff   |
                  | osm2pgsql  | ---> database
                  | -e 5-12 -a |      update
                | | ---> new tiles

In the diagram some of the files are shown with conventional extensions (.osm and .osc) although compressed and binary formats are being used (.osc.gz, .o5m, .o5c).

Involved Files
  • hourly.osc: hourly OsmChange file, downloaded from
  • p.poly: border polygon of our graphical region
  • a.osm: previous OSM data file (all data of our geographical region)
  • b.osm: updated OSM data file (all data of our geographical region)
  • fa.osm: previous filtered OSM data file (only thematic data, e.g. public transport)
  • fb.osm: filtered new OSM data file (only thematic data, e.g. public transport)
  • gis.osc: the OsmChange file (so-called diff file) you would need to update fa.osm to fb.osm
  • dirty_tiles: a list of all tiles which are affected by the gis.osc and therefore have to be rerendered
Used Programs
  • osmupdate: download .osc files and use them to update an .osm file
  • osmfilter: filter OSM data and discard all the information we do not need
  • osmconvert: compare the difference between two .osm files and create an .osc diff file
  • osm2pgsql: update a postgreSQL geo database and calculate a dirty-tiles list
  • read the dirty-tiles file and (re)render all listed tiles

Most of the task also can be done – maybe more reliable – with Osmosis. If you already have installed Osmosis, feel free to use it. Osmosis offers a much wider variety of functions but will be slower.

How-To Implement the Alternative Strategy

Please note that this strategy has not been tested yet. Please expect errors and unexpected behaviour. Nevertheless, the following sections will give you a brief description how one might implement the strategy.

The Usual Steps

Please follow the steps which are described by the chapter #Prepare your System, then continue with the following tasks.

Tool osmupdate

Following this strategy, there is no need to adhere to a fixed update timetable. Hence we decide to use not only weekly or daily downloads but also the hourly .osc files. The program osmupdate will help us to keep the local OSM data files up-to-date.

To download and compile osmupdate, enter these commands:

cd /home/pt
wget -O - |cc -x c - -o osmupdate

For further details on this tool refer to osmupdate.

Adapt osm2pgsql

As described above, we will have to adapt one of osm2pgsql's source files. The following changes will have to be made (for osm2pgsql version 0.80.0). Please create a new file:

cd /home/pt
cd osm2pgsql
nano expire-tiles.diff

Now copy this contents into the new file:

*** expire-tiles_0_80_0.c    2011-05-21 15:13:49.000000000 +0200
--- expire-tiles.c    2011-08-10 16:23:05.000000000 +0200
*************** static int _mark_tile(struct tile ** tre
*** 105,114 ****
--- 105,146 ----
  static int mark_tile(struct tile ** tree_head, int x, int y, int zoom) {
  	return _mark_tile(tree_head, x, y, zoom, 0);
+ #if 1
+ static void output_dirty_tile(FILE * outfile, int x, int y, int zoom, int min_zoom) {
+   // writes a tile into output file, including all subsequent tiles
+   // with higher zoom levels;
+   int x_max, y_max;
+   int tile_size;
+   tile_size= 1;
+   while (zoom <= Options->expire_tiles_zoom) {
+     if(zoom>=min_zoom) {
+       x_max = x + tile_size;
+       while (x < x_max) {
+         y_max = y + tile_size;
+         while (y < y_max) {
+ 		      if ((outcount++ % 1000) == 0) {
+ 			      fprintf(stderr, "\rWriting dirty tile list (%ik)", outcount / 1000);
+ 			      fflush(stderr);
+ 		      }
+ 		      fprintf(outfile, "%i/%i/%i\n", zoom, x, y);
+           y++;
+         }
+         y-= tile_size;
+         x++;
+       }
+       x-= tile_size;
+     }
+     zoom++;
+     x<<= 1; y<<= 1;
+     tile_size<<= 1;
+   }
+ }
+ #else
  static void output_dirty_tile(FILE * outfile, int x, int y, int zoom, int min_zoom) {
  	int	y_min;
  	int	x_iter;
  	int	y_iter;
  	int	x_max;
*************** static void output_dirty_tile(FILE * out
*** 131,148 ****
--- 163,191 ----
  			fprintf(outfile, "%i/%i/%i\n", out_zoom, x_iter, y_iter);
+ #endif
  static void _output_and_destroy_tree(FILE * outfile, struct tile * tree, int x, int y, int this_zoom, int min_zoom) {
  	int	sub_x = x << 1;
  	int	sub_y = y << 1;
  	FILE *	ofile;
  	if (! tree) return;
+ #if 1
+   if(this_zoom >= min_zoom) {
+     if ((outcount++ % 1000) == 0) {
+       fprintf(stderr, "\rWriting dirty tile list (%ik)", outcount / 1000);
+       fflush(stderr);
+     }
+     fprintf(outfile, "%i/%i/%i\n", this_zoom, x, y);
+   }
+ #endif
  	ofile = outfile;
  	if ((tree->complete[0][0]) && outfile) {
  		output_dirty_tile(outfile, sub_x + 0, sub_y + 0, this_zoom + 1, min_zoom);
  		ofile = NULL;

Use these commands to apply the changes and to recompile osm2pgsql's source:

patch -b expire-tiles.c expire-tiles.diff

Feed Mapnik with a Tiles List

Mapnik must be enabled to read and process dirty-tiles lists. The following python script will exactly do this. Enter /home/pt/mapnik directory and create a file with the name and the following contents:

# mapnik_tile 2011-08-20 13:40
# read dirty-tiles file and render each tile of this list;
# afterwards, delete this file;
# call: DTF="dirty_tiles"
# parallel call: DTF="dirty_tiles" PID=123
# License: AGPL
# (c) Markus Weber, Nuernberg
from math import pi,cos,sin,log,exp,atan
from subprocess import call
import sys, os
from Queue import Queue
import mapnik
import threading
import shutil

DEG_TO_RAD = pi/180
RAD_TO_DEG = 180/pi

def minmax (a,b,c):
    a = max(a,b)
    a = min(a,c)
    return a

class GoogleProjection:
    def __init__(self,levels=18):
        self.Bc = []
        self.Cc = []
        self.zc = []
        self.Ac = []
        c = 256
        for d in range(0,levels):
            e = c/2;
            self.Cc.append(c/(2 * pi))
            c *= 2

    def fromLLtoPixel(self,ll,zoom):
         d = self.zc[zoom]
         e = round(d[0] + ll[0] * self.Bc[zoom])
         f = minmax(sin(DEG_TO_RAD * ll[1]),-0.9999,0.9999)
         g = round(d[1] + 0.5*log((1+f)/(1-f))*-self.Cc[zoom])
         return (e,g)

    def fromPixelToLL(self,px,zoom):
         e = self.zc[zoom]
         f = (px[0] - e[0])/self.Bc[zoom]
         g = (px[1] - e[1])/-self.Cc[zoom]
         h = RAD_TO_DEG * ( 2 * atan(exp(g)) - 0.5 * pi)
         return (f,h)

if __name__ == "__main__":

  # read environment variables
  dt_file_name = os.environ['DTF']
    temp_file = "/dev/shm/tile" + os.environ['PID']
    temp_file = "/dev/shm/tile"

  # do some global initialization
  print "mapnik_tile " + dt_file_name + ": started."
  mapfile = "mapnik_pt.xml"
  tile_dir = "/var/www/tiles/"
  max_zoom = 18
  tile_count = 0
  empty_tile_count = 0

  # create tile directory and all possible zoom directories
  if not os.path.isdir(tile_dir):
  for z in range(0, max_zoom):
    d = tile_dir + "%s" % z + "/"
    if not os.path.isdir(d):

  # open the dirty-tiles file
    dt_file = file(dt_file_name, "r")
    dt_file = None
  if (dt_file != None):

    # do some Mapnik initialization
    mm = mapnik.Map(256, 256)
    mapnik.load_map(mm, mapfile, True)
    # Obtain <Map> projection
    prj = mapnik.Projection(mm.srs)
    # Projects between tile pixel co-ordinates and LatLong (EPSG:4326)
    tileproj = GoogleProjection(max_zoom + 1)

    # process the dirty-tiles file
    while True:

      # read a line of the dirty-tiles file
      line = dt_file.readline()
      if (line == ""):
      line_part = line.strip().split('/')

      # create x coordinate's directory - if necessary
      d = tile_dir + line_part[0] + "/" + line_part[1] + "/"
      if not os.path.isdir(d):

      # get parameters of the line
      z = int(line_part[0])
      x = int(line_part[1])
      y = int(line_part[2])
      tile_name= tile_dir + line[:-1] + ".png"

      # render this tile - start

      # Calculate pixel positions of bottom-left & top-right
      p0 = (x * 256, (y + 1) * 256)
      p1 = ((x + 1) * 256, y * 256)

      # Convert to LatLong (EPSG:4326)
      l0 = tileproj.fromPixelToLL(p0, z);
      l1 = tileproj.fromPixelToLL(p1, z);

      # Convert to map projection (e.g. mercator co-ords EPSG:900913)
      c0 = prj.forward(mapnik.Coord(l0[0], l0[1]))
      c1 = prj.forward(mapnik.Coord(l1[0], l1[1]))

      # Bounding box for the tile
      if hasattr(mapnik,'mapnik_version') and mapnik.mapnik_version() >= 800:
        bbox = mapnik.Box2d(c0.x,c0.y, c1.x,c1.y)
      	bbox = mapnik.Envelope(c0.x,c0.y, c1.x,c1.y)
      render_size = 256
      mm.resize(render_size, render_size)
      mm.buffer_size = 256
      # (buffer size was 128)

      # render image with default Agg renderer
      im = mapnik.Image(render_size, render_size)
      mapnik.render(mm, im), 'png256')
      tile_count = tile_count + 1

      # render this tile - end

      # copy this tile to tile tree
      l= os.stat(temp_file)[6]
      if l>116:
          l= l  # (tile did not exist)
        empty_tile_count = empty_tile_count + 1

      # print progress information
      # if (tile_count % 250) == 0:
        # print "mapnik_tile " + dt_file_name + ": Progress:", tile_count, "tiles."
        # sys.stdout.flush()

    # close and delete the dirty-tiles file
      l= l  # (file did not exist)

  # print some statistics
  print "mapnik_tile " + dt_file_name + ":", tile_count, "tiles (" + "%s" % empty_tile_count + " empty)."

Create an Empty Tile Image

OpenLayers must be advised what to do if an tile image is to be loaded which does not exist on the server. Fortunately, we already did the necessary changes to the file index.html (see above, look for emptytile.png). Now we must create such an "empty tile" and store it as .png file. Just enter this command:

echo -en "\x89PNG\r\n\x1a\n...\rIHDR..;...;.;\x03...f\xbc:"\
"\x21..;\x9a\x60\xe1\xd5....IEND\xaeB\x60\x82" |\
tr ".;" "\000\001" >/var/www/emptytile.png

Toolchain for the Alternative Strategy

First prepare an osm filter parameter file. Open a new file with the name toolchain_filter with the editor and insert the following text:

route=ferry =railway =train =light_rail =subway =tram =trolleybus =bus =funicular =aerialway
line=ferry =railway =rail =train =light_rail =subway =tram =trolleybus =bus =funicular
public_transport=stop_area =stop_position
railway=station =halt =tram_stop


railway=station =halt =tram_stop
aerialway=station =yes
ferry=yes train=yes subway=yes
monorail=yes tram=yes trolleybus=yes bus=yes
ref= uic_ref= ref_name= name= website= wheelchair=

route= line= type=

Now use the same method to create the toolchain script, name it

# (c) Markus Weber, 2011-09-18 18:10, 2012-01-20
# License: AGPL
# This script cares about creating a thematical map.
# It loads and updates the map data and renders the tiles.
# The script will run endless in a loop. To terminate it,
# please delete the file "toolchain_running.txt".
PLANETMINSIZE=60000000  # minimum size of OSM data file in .o5m format
MAXPROCESS=5  # maximum number of concurrent processes for rendering
OSM2PGSQLPARAM="-s -C 3000 -d ptgis -U ptuser -S"
  # main parameters to be passed to osm2pgsql

# enter working directory and do some initializations
cd /home/pt
echo >>tc.log
echo $(date)"  toolchain started." >>tc.log
PROCN=1000  # rendering-process number (range 1000..1999)
mkdir d_t 2>/dev/null

# publish that this script is now running
rm toolchain_ended.txt 2>/dev/null
echo -e "The toolchain script has been started at "$(date)"\n"\
"and is currently running. To terminate it, delete this file and\n"\
"wait until the file \"toolchain_ended.txt\" has been created.\n"\
"This may take some minutes." \

# clean up previous Mapnik processes
killall "" 2>/dev/null
while [ $(ls d_t/at* 2>/dev/null |wc -l) -gt 0 ]; do
    # there is at lease one incompleted rendering process
  AT=$(ls -1 d_t/at* 2>/dev/null|head -1)  # tile list
  echo $(date)"  Cleaning up incomplete rendering: "$AT >>tc.log
  DT=${AT:0:4}d${AT:5:99}  # new name of the tile list
  mv $AT $DT  # rename this tile list file;
    # now the tile ist is market as 'to be rendered'

# download and process planet file - if necessary
if [ "0"$(stat --print %s a.o5m 2>/dev/null) -lt $PLANETMINSIZE ]; then
  echo $(date)"  Missing file a.o5m, downloading it." >>tc.log
  rm -f fa.o5m
  wget -nv $PLANETURL -O - 2>>tc.log |./osmconvert - \
    $BORDERS --out-o5m >a.o5m 2>>tc.log
  echo $(date)"  Updating the downloaded planet file." >>tc.log
  rm -f b.o5m
  ./osmupdate -v a.o5m b.o5m --keep-tempfiles \
    --max-merge=15 $BORDERS --drop-author >>tc.log 2>&1
  if [ "0"$(stat --print %s b.o5m 2>/dev/null) -gt $PLANETMINSIZE ]; then
    echo $(date)"  Update was successful." >>tc.log
  mv -f b.o5m a.o5m
  if [ "0"$(stat --print %s a.o5m 2>/dev/null) -lt $PLANETMINSIZE ]; then
    echo $(date)"  toolchain Error: could not download"\
      $PLANETURL >>tc.log
    exit 1

# refill the database - if necessary
if [ "0"$(stat --print %s fa.o5m 2>/dev/null) -lt 5 ]; then
  echo $(date)"  Missing file fa.o5m, creating it." >>tc.log
  rm dirty_tiles d_t/* 2>/dev/null
  echo $(date)"  Filtering the downloaded planet file." >>tc.log
  ./osmfilter a.o5m --parameter-file=toolchain_filter --out-o5m \
    >fa.o5m 2>>tc.log  # filter the planet file
  ./osmconvert fa.o5m --out-osm >gis.osm 2>>tc.log
    # convert to .osm format
  echo $(date)"  Writing filtered data into the database." >>tc.log
  ./osm2pgsql/osm2pgsql $OSM2PGSQLPARAM -c gis.osm -e 4-17 >/dev/null 2>&1
    # enter filtered planet data into the database
  echo $(date)"  All tiles need to be rerendered!" >>tc.log
  echo $(date)"  If the tile directory is not empty, please remove" >>tc.log
  echo $(date)"  all outdated tile files." >>tc.log

# main loop
while [ -e "toolchain_running.txt" ]; do
  echo $(date)"  Processing main loop." >>tc.log

  # limit log file size
  if [ "0"$(stat --print %s tc.log 2>/dev/null) -gt 1500000 ]; then
    echo $(date)"  Reducing logfile size." >>tc.log
    mv tc.log tc.log_temp
    tail -c +1000000 tc.log_temp |tail -n +2 >tc.log
    rm tc.log_temp 2>/dev/null

  #care about entries in dirty-tile list
  while [ $(ls dirty_tiles d_t/?t* 2>/dev/null |wc -l) -gt 0 -a \
      -e "toolchain_running.txt" ]; do
      # while still tiles to render

    # start as much render processes as allowed
    while [ $(ls d_t/dt* 2>/dev/null |wc -l) -gt 0 -a \
        $(ls -1 d_t/at* 2>/dev/null |wc -l) -lt $MAXPROCESS ]; do
        # while dirty tiles in list AND process slot(s) available
      DT=$(ls -1 d_t/dt* |head -1)  # tile list
      AT=${DT:0:4}a${DT:5:99}  # new name of the tile list
      mv $DT $AT  # rename this tile list file;
        # this is our way to mark a tile list as 'being rendered now'
      #echo $(date)"  Rendering "$DT >>tc.log
      PROCN=$(($PROCN + 1))
      if [ $PROCN -gt 1999 ]; then
        PROCN=1000;  # (force range to 1000..1999)
      N=${PROCN:1:99}  # get last 3 digits
      cd mapnik
      DTF=../$AT PID=$N nohup ./ >/dev/null 2>&1 &
        # render every tile in list
      cd ..
      echo $(date)"  Now rendering:"\
        $(ls -m d_t/at* 2>/dev/null|tr -d "d_t/a ") \
      sleep 2  # wait a bit

    # determine if we have rendered all tiles of all lists
    if (ls d_t/?t* >/dev/null 2>&1); then  # still tiles to render
      sleep 11  # wait some seconds
  continue  # care about rendering again
    # here: we have rendered all tiles of all lists

    # care about dirty-tiles master list and split it into parts
    if (ls dirty_tiles >/dev/null 2>&1); then
        # there is a dirty-tiles master list
      echo $(date)"  Splitting dirty-tiles list" \
        "("$(cat dirty_tiles |wc -l)" tiles)" >>tc.log
      split -l 1000 -d -a 6 dirty_tiles d_t/dt
      echo "*** "$(date) >>dt.log
      cat dirty_tiles >>dt.log  # add list to dirty-tiles log
      rm dirty_tiles 2>/dev/null
      # limit dirty-tiles log file size
      if [ "0"$(stat --print %s dt.log 2>/dev/null) -gt 750000000 ]; then
        echo $(date)"  Reducing dirty-tiles logfile size." >>tc.log
        mv dt.log dt.log_temp
        tail -c +500000000 dt.log_temp |tail -n +2 >dt.log
        rm dt.log_temp 2>/dev/null

    done  # while still tiles to render
  if [ ! -e "toolchain_running.txt" ]; then  # script shall be terminated
continue;  # exit the main loop via while statement
  # here: all tiles have been rendered

  # update the local planet file
  echo $(date)"  Updating the local planet file." >>tc.log
  rm b.o5m fb.o5m a.o5c 2>/dev/null
  ./osmupdate a.o5m b.o5m --daily \
    --max-merge=15 $BORDERS --drop-author -v >>tc.log 2>&1
  if [ "0"$(stat --print %s b.o5m 2>/dev/null) -lt \
      $(expr "0"$(stat --print %s a.o5m 2>/dev/null) \* 9 / 10) ]; then
      # if new osm file smaller than 90% of the old file's length
    # wait a certain time and retry the update
    while [ $I -lt 33 -a -e "toolchain_running.txt" ]; do  # (33 min)
      sleep 60
      I=$(( $I + 1 ))

  # filter the new planet file
  echo $(date)"  Filtering the new planet file." >>tc.log
  ./osmfilter b.o5m --parameter-file=toolchain_filter --out-o5m >fb.o5m \

  # calculate difference between old and new filtered file
  echo $(date)"  Calculating diffs between old and new filtered file."\
  ./osmconvert fa.o5m fb.o5m --diff-contents --fake-lonlat --out-osc \
    >gis.osc 2>>tc.log

  # check if the diff file is too small
  if [ "0"$(stat --print %s gis.osc 2>/dev/null) -lt 50 ]; then
    echo $(date)"  Error: diff file is too small." >>tc.log
    exit 2

  # enter differences into the database
  echo $(date)"  Writing differential data into the database." >>tc.log
  ./osm2pgsql/osm2pgsql $OSM2PGSQLPARAM -a gis.osc -e 4-17 >/dev/null 2>&1

  # replace old files by the new ones
  echo $(date)"  Replacing old files by the new ones." >>tc.log
  ls -lL a.o5m fa.o5m b.o5m fb.o5m gis.osm gis.osc >>tc.log
  mv -f a.o5m a_old.o5m
  mv -f fa.o5m fa_old.o5m
  mv -f b.o5m a.o5m
  mv -f fb.o5m fa.o5m

  # check if there are any tiles affected by this update
  if [ "0"$(stat --print %s dirty_tiles 2>/dev/null) -lt 5 ]; then
    echo $(date)"  There are no tiles affected by this update." >>tc.log
    rm -f dirty_tiles

  done  # main loop

# wait until every rendering process has terminated
while (ls d_t/at* 2>/dev/null) ; do sleep 30; done

# publish that this script has ended
rm toolchain_running.txt 2>/dev/null
echo "The toolchain script ended at "$(date)"." \

Further Strategies

Mapnik experts will have noticed that the rendering strategies which have been introduced on this page do not represent the standard solution. Most Slippy Map installations will use the more comfortable Tirex/Modtile tools to define which tiles when to render. Furthermore, of course, you usually do not render regularly sized tiles but so-called meta tiles instead, which are 16 or 64 times larger. On standard maps this increases rendering speed significantly. It would be interesting to know if there are advantages for diff based rendered thematic maps too because you would have to render a lot of areas which have not changed.

Please feel free to add your experiences here if you have tried different methods and had the opportunity to compare them.


Be prepared that in the first run every affected tile of your thematic layer will be rendered. This may take between one hour and a few weeks, depending on the available hardware, the size of the chosen geographical region and the density of the thematic data.

The planet-wide public transport map, for example, took about four days to be initially rendered on a quad core CPU. The statistics in detail:

  • 1 hour downloading and converting the planet file
  • 30 minutes initially updating the planet file
  • 20 minutes filtering the planet file
  • 40 minutes writing the filtered data to the database
  • 4 days rendering all the tiles of the thematic layer

Map updates run faster. An average updates takes about 15 hours. In detail (example):

  • 20 minutes updating the planet file
  • 20 minutes filtering the new planet file
  • 8 seconds calculating the differences between old and new filtered data
  • 10..20 minutes writing the differences of the filtered data to the database
  • 10..30 hours rerendering the changed tiles of the thematic layer


Too many links

If this message occurs during rendering, you most likely are using a file system which supports only a limited number of subdirectories (e.g. ext2 or ext3). Please upgrade to ext4. The migration from ext3 to ext4 should be possible without any loss of data. Please refer to the users guide of your operating system.