HOWTO: Install OpenTopoMap on Ubuntu 24.04 LTS (Part 1)

This comprehensive guide updates and modernizes the original HOWTO install OpenTopoMap on Ubuntu 18.04 LTS. It walks you through system setup, database configuration, DEM preparation, and tile rendering.

Update and Clean Up Your System

sudo add-apt-repository main
sudo add-apt-repository restricted
sudo add-apt-repository universe
sudo add-apt-repository multiverse
sudo apt update && sudo apt full-upgrade

Clean up the system:

# 1. Ensure all official repos are enabled
sudo add-apt-repository -y main universe multiverse restricted

# 2. Fix partial installations
sudo dpkg --configure -a
sudo apt -f install

# 3. Refresh everything
sudo apt update
sudo apt full-upgrade -y

# 4. Remove orphaned / half-installed stuff
sudo apt autoremove --purge -y
sudo apt clean

Install Required Packages

sudo apt install git tar unzip wget bzip2 build-essential autoconf libtool pkg-config   libxml2-dev libgeos-dev libgeos++-dev libpq-dev libbz2-dev   libproj-dev proj-bin proj-data   munin-node munin   libprotobuf-c-dev protobuf-c-compiler  libfreetype-dev libpng-dev libtiff-dev libicu-dev libgdal-dev   libcairo2-dev libcairomm-1.0-dev   apache2 apache2-dev   libagg-dev   lua5.3 liblua5.3-dev liblua5.1-0-dev   fonts-unifont   libgeotiff-dev   cmake   devscripts libjson-perl libipc-sharelite-perl libgd-perl debhelper gdal-bin python3-setuptools  python3-matplotlib  python3-bs4  python3-nump python3-pip python3-lxml python3-gdal wget unzip pipx osm2pgsql python3-mapnik libmapnik-dev mapnik-utils

Install some more useful packages:

sudo apt install vim git screen htop iptraf

Install PosgreSQL and PostGIS

sudo apt install -y postgresql postgresql-16 postgresql-contrib postgresql-16-postgis-3 postgresql-16-postgis-3-scripts

Start and enable the service:

sudo systemctl enable postgresql
sudo systemctl start postgresql
sudo systemctl status postgresql

Create a database user for yourself:

sudo -u postgres createuser -s $USER

Create a GIS database:

createdb -O $USER gis
psql -d gis -c "CREATE EXTENSION postgis;"
psql -d gis -c "CREATE EXTENSION hstore;"

Clone the OpenTopoMap Repository

cd
git clone https://github.com/der-stefan/OpenTopoMap.git
cd OpenTopoMap/mapnik

Download Water Polygons

Get the generalized water polygons from http://openstreetmapdata.com/

mkdir data && cd data
# simplified polygons (replaces water‑polygons‑generalized‑3857.zip)
wget https://osmdata.openstreetmap.de/download/simplified-water-polygons-split-3857.zip

# unsimplified split polygons
wget https://osmdata.openstreetmap.de/download/water-polygons-split-3857.zip

# optional Antarctic datasets if you need polar coverage
# wget https://osmdata.openstreetmap.de/download/antarctica-icesheet-polygons-3857.zip
# wget https://osmdata.openstreetmap.de/download/antarctica-icesheet-outlines-3857.zip

# unpack the shapefiles
unzip water-polygons-split-3857.zip
unzip simplified-water-polygons-split-3857.zip
# unzip antarctica-icesheet-polygons-3857.zip
# unzip antarctica-icesheet-outlines-3857.zip

Prepare the DEM (Digital Elevation Model)

Indentify the elevation data you need: www.viewfinderpanoramas.org

Create an STRM download list:

mkdir ~/srtm
cd ~/srtm
sudo vi list.txt

Insert what you need:

http://viewfinderpanoramas.org/dem3/N31.zip
http://viewfinderpanoramas.org/dem3/N32.zip
http://viewfinderpanoramas.org/dem3/M31.zip
http://viewfinderpanoramas.org/dem3/M32.zip
http://viewfinderpanoramas.org/dem3/L31.zip
http://viewfinderpanoramas.org/dem3/L32.zip

Download the data:

wget -i list.txt

Unzip:

for zipfile in *.zip;do unzip -j -o "$zipfile" -d unpacked; done

Fill the voids:

cd unpacked
for hgtfile in *.hgt;do gdal_fillnodata.py $hgtfile $hgtfile.tif; done

Merge TIFs:

gdal_merge.py -n 32767 -co BIGTIFF=YES -co TILED=YES -co COMPRESS=LZW -co PREDICTOR=2 -o ../raw.tif *.hgt.tif
rm *hgt.tif

Re-project to Mercator, interpolate and shrink:

cd ..;
gdalwarp -co BIGTIFF=YES -co TILED=YES -co COMPRESS=LZW -co PREDICTOR=2 -t_srs "+proj=merc +ellps=sphere +R=6378137 +a=6378137 +units=m" -r bilinear -tr 5000 5000 raw.tif warp-5000.tif
gdalwarp -co BIGTIFF=YES -co TILED=YES -co COMPRESS=LZW -co PREDICTOR=2 -t_srs "+proj=merc +ellps=sphere +R=6378137 +a=6378137 +units=m" -r bilinear -tr 1000 1000 raw.tif warp-1000.tif
gdalwarp -co BIGTIFF=YES -co TILED=YES -co COMPRESS=LZW -co PREDICTOR=2 -t_srs "+proj=merc +ellps=sphere +R=6378137 +a=6378137 +units=m" -r bilinear -tr 700 700 raw.tif warp-700.tif
gdalwarp -co BIGTIFF=YES -co TILED=YES -co COMPRESS=LZW -co PREDICTOR=2 -t_srs "+proj=merc +ellps=sphere +R=6378137 +a=6378137 +units=m" -r bilinear -tr 500 500 raw.tif warp-500.tif
gdalwarp -co BIGTIFF=YES -co TILED=YES -co COMPRESS=LZW -co PREDICTOR=2 -t_srs "+proj=merc +ellps=sphere +R=6378137 +a=6378137 +units=m" -r bilinear -tr 90 90 raw.tif warp-90.tif

Create color relief:

gdaldem color-relief -co COMPRESS=LZW -co PREDICTOR=2 -alpha warp-5000.tif ../OpenTopoMap/mapnik/relief_color_text_file.txt relief-5000.tif
gdaldem color-relief -co COMPRESS=LZW -co PREDICTOR=2 -alpha warp-500.tif ../OpenTopoMap/mapnik/relief_color_text_file.txt relief-500.tif

Create hillshade:

gdaldem hillshade -z 7 -compute_edges -co COMPRESS=JPEG warp-5000.tif hillshade-5000.tif
gdaldem hillshade -z 7 -compute_edges -co BIGTIFF=YES -co TILED=YES -co COMPRESS=JPEG warp-1000.tif hillshade-1000.tif
gdaldem hillshade -z 4 -compute_edges -co BIGTIFF=YES -co TILED=YES -co COMPRESS=JPEG warp-700.tif hillshade-700.tif
gdaldem hillshade -z 4 -compute_edges -co BIGTIFF=YES -co TILED=YES -co COMPRESS=JPEG warp-500.tif hillshade-500.tif
gdaldem hillshade -z 2 -co compress=lzw -co predictor=2 -co bigtiff=yes -compute_edges warp-90.tif hillshade-90.tif && gdal_translate -co compress=JPEG -co bigtiff=yes -co tiled=yes hillshade-90.tif hillshade-90-jpeg.tif
gdaldem hillshade -z 5 -compute_edges -co BIGTIFF=YES -co TILED=YES -co COMPRESS=JPEG warp-90.tif hillshade-30m-jpeg.tif

Edit opentopomap.xml to use planet_osm_line:

        <Layer name="contours">
                <StyleName>contours</StyleName>
                <Datasource>
                        <!-- If you imported the contour lines as suggested in the HOWTO_DEM, use following: -->
            <Parameter name="table">(SELECT way,ele FROM planet_osm_line) AS contours </Parameter>
            <!-- <Parameter name="table">(SELECT way,ele FROM contours) AS contours </Parameter> -->
                        <Parameter name="dbname">contours</Parameter>
                        &postgis-settings;
                </Datasource>
        </Layer>

Generate Contours with pyhgtmap

Install pyhgtmap (fork of phyghtmap):

pipx ensurepath
pipx install --system-site-packages pyhgtmap
# pipx install --force pyhgtmap[geotiff]
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Create contour lines:

pyhgtmap -o contour --max-nodes-per-tile=0 -s 10 -0 --pbf warp-90.tif

Make sure the TIF files reside in mapnik/dem, e.g.:

mkdir ~/OpenTopoMap/mapnik/dem
mv *tif ~/OpenTopoMap/mapnik/dem/

Create the “contours” database:

sudo -u postgres psql -c "DROP DATABASE IF EXISTS contours;"
createdb -O $USER contours
psql -d contours -c "CREATE EXTENSION postgis;"
psql -d contours -c "CREATE EXTENSION hstore;"

Load the data:

osm2pgsql --slim -d contours --cache 5000 --style ../OpenTopoMap/mapnik/osm2pgsql/contours.style contour*.pbf

Import OSM Data

mkdir ~/data
cd ~/data
wget https://download.geofabrik.de/europe/germany/nordrhein-westfalen-latest.osm.pbf
screen osm2pgsql --slim -d gis -C 12000 --number-processes 10   --flat-nodes /mnt/database/flat-nodes/gis-flat-nodes.bin   --style ~/OpenTopoMap/mapnik/osm2pgsql/opentopomap.style   ~/data/nordrhein-westfalen-latest.osm.pbf

Preprocessing and Rendering

Compile helper tools:

cd ~/OpenTopoMap/mapnik/tools
cc -o saddledirection saddledirection.c -lm -lgdal
cc -Wall -o isolation isolation.c -lgdal -lm -O2

Run preprocessing scripts:

psql gis < arealabel.sql 
screen ./update_lowzoom.sh
ln -s  ~/OpenTopoMap/mapnik/dem/raw.tif ~/OpenTopoMap/mapnik/dem/dem-srtm.tiff
screen ./update_saddles.sh 
screen ./update_parking.sh
screen ./update_isolations.sh
psql gis < stationdirection.sql
psql gis < viewpointdirection.sql
psql gis < pitchicon.sql

Create mapnik_render_tile.py for Python 3:


#!/usr/bin/env python3
"""
Render a single PNG tile with Mapnik to verify your DB/style setup.

Usage (example):
  python3 mapnik_render_tile.py --style opentopomap.xml --out opentopomap_output.png \
    --z 13 --x 1320 --y 2860

Defaults match the old test.
"""

import os
import sys
import argparse
import mapnik


def tile2prjbounds(settings, x, y, z):
    """
    Compute projected bounds (EPSG:3857) for a Z/X/Y tile region.

    :param settings: geometry settings dict
    :param x: tile x
    :param y: tile y
    :param z: tile zoom
    :return: (x0, y0, x1, y1)
    """
    render_size_tx = min(8, settings['aspect_x'] * (1 << z))
    render_size_ty = min(8, settings['aspect_y'] * (1 << z))

    prj_width  = settings['bound_x1'] - settings['bound_x0']
    prj_height = settings['bound_y1'] - settings['bound_y0']

    p0x = settings['bound_x0'] + prj_width  * (float(x) / (settings['aspect_x'] * (1 << z)))
    p0y = settings['bound_y1'] - prj_height * ((float(y) + render_size_ty) / (settings['aspect_y'] * (1 << z)))
    p1x = settings['bound_x0'] + prj_width  * ((float(x) + render_size_tx) / (settings['aspect_x'] * (1 << z)))
    p1y = settings['bound_y1'] - prj_height * (float(y) / (settings['aspect_y'] * (1 << z)))

    return p0x, p0y, p1x, p1y


def register_fonts(extra_dirs=None):
    """Register system fonts (and optional extra dirs) for Mapnik."""
    try:
        # Register distro/system fonts (works on Ubuntu 24.04)
        mapnik.register_system_fonts()
    except Exception:
        # Older Mapniks may not have this helper; ignore if missing
        pass

    # Register additional directories (recursively)
    for d in (extra_dirs or []):
        if os.path.isdir(d):
            try:
                mapnik.register_fonts(d, True)
            except Exception as e:
                print(f"[warn] Could not register fonts in {d}: {e}", file=sys.stderr)

    # Also register common locations explicitly (recursive)
    for d in ("/usr/share/fonts",
              "/usr/local/share/fonts",
              os.path.expanduser("~/.local/share/fonts")):
        if os.path.isdir(d):
            try:
                mapnik.register_fonts(d, True)
            except Exception as e:
                print(f"[warn] Could not register fonts in {d}: {e}", file=sys.stderr)


def main():
    parser = argparse.ArgumentParser(description="Render a PNG tile with Mapnik.")
    parser.add_argument("--style", "-s", default="opentopomap.xml", help="Mapnik XML stylesheet")
    parser.add_argument("--out", "-o", default="opentopomap_output.png", help="Output PNG path")
    parser.add_argument("--width", type=int, default=2048, help="Image width in pixels")
    parser.add_argument("--height", type=int, default=2048, help="Image height in pixels")
    parser.add_argument("--z", type=int, default=13, help="Zoom")
    parser.add_argument("--x", type=int, default=1320, help="Tile X")
    parser.add_argument("--y", type=int, default=2860, help="Tile Y")
    args = parser.parse_args()

    if not os.path.exists(args.style):
        print(f"[error] Stylesheet not found: {args.style}", file=sys.stderr)
        sys.exit(1)

    register_fonts()

    m = mapnik.Map(args.width, args.height)
    mapnik.load_map(m, args.style)

    # Web Mercator bounds in meters
    geom_settings = {
        'bound_x0': -20037508.3428,
        'bound_x1':  20037508.3428,
        'bound_y0': -20037508.3428,
        'bound_y1':  20037508.3428,
        'aspect_x': 1.0,
        'aspect_y': 1.0,
    }

    p0x, p0y, p1x, p1y = tile2prjbounds(geom_settings, args.x, args.y, args.z)
    bbox = mapnik.Box2d(p0x, p0y, p1x, p1y)
    m.zoom_to_box(bbox)

    print(f"[info] Envelope: {m.envelope()}")
    print(f"[info] Scale: {m.scale()}")

    mapnik.render_to_file(m, args.out)
    print(f"[ok] Wrote {args.out}")


if __name__ == "__main__":
    main()

Render a test tile:

cd ~/OpenTopoMap/mapnik/
python3 ./mapnik_render_tile.py

Done!

Your OpenTopoMap rendering environment is ready. You can now generate tiles and explore global topographic rendering with contours, hillshades, and water layers—all powered by open data.