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.
Contents
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.