OpenStreetMap/SRTM 3D display

What ?

I wanted to display OpenStreetMap hiking paths on 3D terrain to get a better idea of hiking routes.

How ?

I used python and some nice libraries (Mayavi mlab, gdal, Numpy, and Osm Python API) to learn how to do this. This is pretty much a work-in-(very slow)-progress. I used many ressources to make this (see bookmarks).

I had to make a custom version of OSM Python API to add a basic http proxy support.

I selected a hiking trail near Grenoble in OpenStreetMap, clicked on “edit” and got the ID of the path. The relevant SRTM file was downloaded from here to get the 3D elevation data.

Next steps should be :

  • download and render more than just one hiking path (using OsmApi, that should be easy enough)
  • get better elevation data (hard…, seems IGN for France is not free to use at 25m resolution)
  • get GPS tracks from OpenStreetMap to get better elevation data on small surfaces (needs an update to Python OSM API)
  • display textures from satellite imaging or equivalent
  • display textures from rendered OSM tiles (including forest/rock/grass…)

Here is the code (comments should be enough to explain)

# -*- coding: utf-8 -*-
# ipython -wthread

from enthought.mayavi import mlab
import numpy
from osgeo import gdal, osr
import OsmApi
import math

earthRadius = 6371000 # Earth radius in meters (yes, it's an approximation)

# Read elevation data file SRTM
# Read GeoTiff and get coordinates
ds = gdal.Open('/home/.../OpenStreetMap/SRTM/srtm_38_03.tif')

width = ds.RasterXSize
height = ds.RasterYSize
gt = ds.GetGeoTransform()
minx = gt[0]
maxx = gt[0] + width*gt[1] + height*gt[2]
miny = gt[3] + width*gt[4] + height*gt[5] 
maxy = gt[3]

band = ds.GetRasterBand(1)

# Grenoble zone to display = 400->1400, 5300->6000 pixels
zoneXmin = 400
zoneXsize = 1000
zoneYmin = 5300
zoneYsize = 695
data = band.ReadAsArray(400, 5300, 1000, 695).astype(numpy.float)

def degrees2metersLongX(latitude, longitudeSpan):
  """ latitude (in degrees) is used to convert a longitude angle to a distance in meters """
  return 2.0*math.pi*earthRadius*math.cos(math.radians(latitude))*longitudeSpan/360.0

def degrees2metersLatY(latitudeSpan):
  """ Convert a latitude angle span to a distance in meters """
  return 2.0*math.pi*earthRadius*latitudeSpan/360.0

def degrees2meters(longitude, latitude):
  return (degrees2metersLongX(latitude, longitude), degrees2metersLatY(latitude))

# CreateX,Y coordinates for data array (contains Z in meters)
linex = numpy.arange(minx+zoneXmin*gt[1], minx+(zoneXmin+zoneXsize)*gt[1], gt[1])[0:zoneXsize]
arrayx = numpy.tile(linex, (len(data), 1)).transpose()

lineydegrees = numpy.arange( maxy+zoneYmin*gt[5], maxy+(zoneYmin+zoneYsize)*gt[5], gt[5])[0:zoneYsize]
liney = numpy.array([degrees2metersLatY(j) for j in lineydegrees])
arrayy = numpy.tile(liney, (len(data[0]), 1))

# Recompute arrayx positions in meters
for x, y in numpy.ndindex(arrayx.shape):
  arrayx[x,y] = degrees2metersLongX(lineydegrees[y], arrayx[x,y])

zscale = 1

# Display 3D surface
mlab.mesh(arrayx, arrayy, data.transpose())

# OpenStreetMap
osm = OsmApi.OsmApi(proxy="******", proxyport = 3128)

# Bec du Corbeau hiking path : --> ID 47189761
way = osm.WayGet(47189761)
nodesId = way[u'nd']
nodes = osm.NodesGet(nodesId)
allX = []
allY = []
allZ = []

for nid in nodes:
  (x,y) = degrees2meters(n['lon'], n['lat'])
  zz = data.transpose()[int(round((n['lon'] - (minx+zoneXmin*gt[1])) / gt[1])), int(round((n['lat'] - (maxy+zoneYmin*gt[5])) / gt[5]))]
  allZ.append(zz+30.0) # We display the path 30m over the surface for it to be visible

# Display path nodes as spheres
mlab.points3d(allX,allY,allZ, color=(1,0,0), mode='sphere', scale_factor=30)
# Displaying the line does not work as nodes are not listed in the correct order
# mlab.plot3d(allX,allY,allZ, color=(1,1,0), line_width=15.0) # representation='surface' 'wireframe' 'points'

# Display a black dot on Grenoble's coordinates
mlab.points3d([5.717*degrees2meters], [45.183*degrees2meters],[200], color=(0,0,0), mode='sphere', scale_factor=50)


  • The three-valleys “Y” of Grenoble


  • A zoomed-in image to see the path


Leave a Reply

Your email address will not be published. Required fields are marked *