A self-contained toolkit for generating contour lines, hillshade, and web-ready MBTiles from USGS Digital Elevation Model (DEM) data.
All paths are relative to the project directory. You can run the script from anywhere — it resolves its own location automatically.
This toolkit processes DEM data to create:
- Contour lines — elevation lines at 40ft intervals (major every 200ft)
- Hillshade — 3D terrain shading with transparency
- MBTiles — web-ready map tiles for TileServer GL, Martin, or similar
One command processes a full state:
python3 process_dem.py --state CTThis will reproject your raw DEM tiles, generate contours and hillshade, clip to the state boundary, and export MBTiles — all automatically.
This toolkit requires Python 3.8+ and several GIS command-line tools. All tools are free and open source.
Ubuntu/Debian:
sudo apt install gdal-bin python3-gdalmacOS (Homebrew):
brew install gdalThis provides all of the following (used by the pipeline):
gdal_contour gdaldem gdalwarp gdal_translate gdalbuildvrt gdaladdo gdalinfo ogr2ogr ogrinfo gdal_calc.py gdal_merge.py gdal_edit.py ogrmerge.py
Verify with:
gdalinfo --version
gdal_calc.py --helpUbuntu/Debian (build from source):
sudo apt install build-essential libsqlite3-dev zlib1g-dev
git clone https://github.com/felt/tippecanoe.git
cd tippecanoe && make -j && sudo make installmacOS (Homebrew):
brew install tippecanoeVerify with:
tippecanoe --versionpip install psutil --break-system-packagesThe pipeline works without it but cannot report detailed memory usage.
terrain_builder/
├── process_dem.py # Main orchestration script
├── scripts/
│ ├── reproject_dem_tiles.py
│ ├── generate_contours.py
│ ├── generate_hillshade.py
│ ├── clip_to_state.py
│ ├── export_mbtiles.py
│ └── resource_monitor.py
├── raw_dem/ # Input: place your DEM .tif files here
├── shape_files/ # Input: state boundary files (e.g., CT.gpkg)
├── reprojected/ # Intermediate: reprojected tiles
├── tiles_vrt/ # Intermediate: non-overlapping VRT tiles
├── tmp/ # Intermediate: auto-cleaned each run
├── output/ # Final clipped outputs (persistent)
│ └── mbtiles/ # Final MBTiles files
├── logs/ # Processing logs
└── USGS_DL_Lists/ # USGS download lists (reference)
Download DEM tiles from the USGS National Map and place the .tif files in raw_dem/.
Place a GeoPackage in shape_files/ named by state code (e.g., CT.gpkg). The GPKG should have a layer named with the state code.
python3 process_dem.py --state CTYou can run the script from any directory:
python3 /path/to/terrain_builder/process_dem.py --state CToutput/
├── contours_CT.gpkg
├── hillshade_CT.tif
└── mbtiles/
├── contours.mbtiles
└── hillshade.mbtiles
# Use more CPU cores for faster processing
python3 process_dem.py --state CT --workers 4
# Use a custom input directory instead of raw_dem/
python3 process_dem.py --state CT --input-dir /path/to/my/dem/files
# Skip cleanup to keep intermediate files (reprojected/, tiles_vrt/)
python3 process_dem.py --state CT --skip-cleanup
# Skip MBTiles export (clip only)
python3 process_dem.py --state CT --skip-export
# Change VRT tile size (default 100km)
python3 process_dem.py --state AK --tile-size 50
# Use a different projection (default: EPSG:3857 Web Mercator)
python3 process_dem.py --state AK --target-srs EPSG:3338python3 process_dem.py --help| Flag | Description |
|---|---|
--state XX |
State code — required (e.g., CT, NY, MA, AK) |
--workers N |
Number of parallel workers (default: 2) |
--target-srs CODE |
Target projection (default: EPSG:3857) |
--tile-size KM |
VRT tile size in km (default: 100) |
--input-dir PATH |
Custom raw DEM directory instead of raw_dem/ |
--skip-cleanup |
Keep intermediate files (reprojected/, tiles_vrt/) |
--skip-export |
Skip MBTiles export stage |
--no-log |
Disable logging to file |
Process multiple states, then merge into unified MBTiles:
# Build each state, skip export until you're done
python3 process_dem.py --state CT --skip-export
python3 process_dem.py --state NY --skip-export
python3 process_dem.py --state MA --skip-export
# Export once at the end — merges all accumulated outputs
python3 scripts/export_mbtiles.py \
--output-dir output/ \
--mbtiles-dir output/mbtiles/The output/ directory accumulates clipped regions across runs. Use --skip-export on each run to avoid redundant MBTiles rebuilds, then export once when you're ready.
| Property | Value |
|---|---|
| Format | GeoPackage (vector) |
| Layer | contours |
| Attributes | elev_m (meters), elev_ft (feet, rounded to 20ft) |
| Major contours | Every 200ft — WHERE elev_ft % 200 = 0 |
| Minor contours | Every 40ft (all others) |
| Property | Value |
|---|---|
| Format | GeoTIFF (raster) |
| Band 1 | Grayscale hillshade (0–255) |
| Band 2 | Alpha channel (transparency) |
| Compression | DEFLATE |
| File | Type | Contents |
|---|---|---|
hillshade.mbtiles |
Raster | Hillshade tiles with overviews |
contours.mbtiles |
Vector (zoom 8–14) | contours_major (200ft) + contours_minor (40ft) |
After processing, copy the MBTiles to your tile server's data directory:
cp output/mbtiles/*.mbtiles /path/to/tileserver/data/Then reload your tile server (TileServer GL, Martin, etc.) to pick up the new tiles.
You can run each step independently for more control. When running scripts individually, cd into the terrain_builder directory first — the sub-scripts use relative paths for their defaults.
cd /path/to/terrain_builderStep 1: Reproject DEM Tiles
python3 scripts/reproject_dem_tiles.py \
--input-dir raw_dem/ \
--output-dir reprojected/ \
--target-srs EPSG:3857 \
--tile-size 100 \
--workers 2Step 2: Generate Contours
python3 scripts/generate_contours.py \
tiles_vrt/ tmp/contours.gpkg \
--workers 2Step 3: Generate Hillshade
python3 scripts/generate_hillshade.py \
tiles_vrt/ tmp/hillshade.tif \
--workers 2Step 4: Clip to State Boundary
python3 scripts/clip_to_state.py \
--state CT \
--gpkg-dir shape_files/ \
--contours-in tmp/contours.gpkg \
--contours-out output/contours_CT.gpkg \
--hillshade-in tmp/hillshade.tif \
--hillshade-out output/hillshade_CT.tifStep 5: Export to MBTiles
python3 scripts/export_mbtiles.py \
--output-dir output/ \
--mbtiles-dir output/mbtiles/The scripts default to 2 workers to stay safe on lower-RAM systems.
| RAM | Recommended --workers |
|---|---|
| 8GB | 2 (default) |
| 16GB | 4–6 |
| 32GB+ | 8–12 |
Typical times for one state (e.g., Connecticut):
| Stage | Time |
|---|---|
| Reprojection | ~2 minutes |
| Contours | ~5 minutes |
| Hillshade | ~5 minutes |
| Clipping | ~1–5 minutes |
| Total | ~10–15 minutes |
The scripts automatically skip tiles with no valid elevation data. You'll see [SKIP] tile_name (no data) — this is normal for coastal areas or regions outside coverage.
Budget approximately 2–5x input size for temporary files and 1–2x for final outputs. The tmp/ directory is auto-cleaned after each run.
Contours:
- Add Vector Layer →
output/contours_CT.gpkg - Style by expression:
"elev_ft" % 200 = 0- Major contours (200ft): thick line, with labels
- Minor contours (40ft): thin line, no labels
Hillshade:
- Add Raster Layer →
output/hillshade_CT.tif - Set blending mode to "Multiply" or "Overlay"
- Place below contours and above base imagery
Each script has detailed help:
python3 process_dem.py --help
python3 scripts/generate_contours.py --help
python3 scripts/generate_hillshade.py --help
python3 scripts/clip_to_state.py --help
python3 scripts/export_mbtiles.py --helpMIT — see LICENSE for details.