2 """Get most accurate elevation for a given point from predefined GDAL data sources or WMS sources.
5 * wrgeo: string with coordinates in Winterrodeln notation, like '47.275712 N 11.3456496 E'
6 * lonlat: tuple with WGS84 referenced longitude, latitude pair in degrees.
7 * xy: projected pair of coordinates
10 https://pcjericks.github.io/py-gdalogr-cookbook/
13 Upper Left ( 73120.000, 247300.000)
14 Lower Left ( 73120.000, 230670.000)
15 Upper Right ( 85220.000, 247300.000)
16 Lower Right ( 85220.000, 230670.000)
17 Center ( 79170.000, 238985.000)
19 gdaltransform -t_srs WGS84 -s_srs EPSG:31254
21 11.3866966549679 47.2939095182383 31.7259142374596
25 (1) Get coordinates from command line in "Winterrodeln" format
26 (2) Transform them to x/y float values
27 (3) Transform WGS84 x/y values to EPSG:31254 x/y value
28 (4) Get the nearest elevation from the DEM map
30 See: file:///usr/share/doc/libgdal-doc/ogr/osr_tutorial.html
34 >>> print(dem.wrgeo_to_ele_tirol('47.275712 N 11.3456496 E'))
36 >>> print(dem.wrgeo_to_ele_vbg('47.435870 N 10.085163 E'))
42 import urllib.request, urllib.parse, urllib.error
43 from urllib.parse import urlencode
44 import xml.etree.ElementTree as et
49 from collections import namedtuple
53 from osgeo.gdalconst import *
55 osgeo.gdal.UseExceptions()
58 BBox = namedtuple('BBox', ['minx', 'maxx', 'miny', 'maxy']) # bounding box. same order as returned by .getEnvelope() of osgeo.ogr geometries
64 def wrgeo_to_lonlat(wrgeo):
65 """Transfers coordinates in the format '47.175712 N 11.1456496 E' to the tuple (11.1456496, 47.175712)."""
66 match = re.match('(\d+\.\d+) N (\d+\.\d+) E', wrgeo)
68 return float(x), float(y)
71 def lonlat_to_epsg(lon, lat, epsg):
72 """lon/lat in WGS84 to specific epsg system like 31254."""
74 srs = osgeo.osr.SpatialReference()
75 srs.SetWellKnownGeogCS('WGS84')
77 srd = osgeo.osr.SpatialReference()
78 srd.ImportFromEPSG(epsg)
80 ct = osgeo.osr.CoordinateTransformation(srs, srd)
81 x, y, z = ct.TransformPoint(lon, lat)
85 def lonlat_to_epsg31254(lon, lat):
86 """lon/lat in WGS84 to MGI_Austria_GK_West (EPSG:31254) conversion.
88 >>> lonlat_to_epsg31254(11.39, 47.29)
89 (80255.74881170085, 239568.73826148175)"""
90 return lonlat_to_epsg(lon, lat, 31254)
93 def lonlat_to_utm32n(lon, lat):
94 """lon/lat in WGS84 to 'UTM Zone 32 North' (EPSG:32632) conversion.
96 >>> lonlat_to_utm32n(11.39, 47.29)
97 (680711.5152423368, 5240161.832015872)"""
98 return lonlat_to_epsg(lon, lat, 32632)
101 def interpolate_bilinear(fx, fy, z00, z10, z01, z11):
102 """Interpolates the elevation between the values z00, z01, z10 and z11.
104 :param fx: fraction between "left" (0) and "right" (1) in x-direction
105 :param fy: fraction between "bottom" (0) and "top" (1) in y-direction
106 :param z00: elevation in the "left bottom" edge (where fx=0, fy=0)
107 :param z10: elevation in the "right bottom" edge (where fx=1, fy=0)
108 :param z01: elevation in the "left upper" edge (where fx=0, fy=1)
109 :param z11: elevation in the "right upper" edge (where fx=1, fy=1)
111 return z00*(1-fx)*(1-fy) + z10*fx*(1-fy) + z01*(1-fx)*fy + z11*fx*fy
114 def degree_to_dms(degree):
116 r = (degree - d) * 60
119 return '{:2d}d{:02d}\'{:4.2f}"'.format(d, m, s)
122 # GDAL supported raster files
123 # ---------------------------
125 # http://www.gdal.org/gdal_datamodel.html says:
126 # "Note that the pixel/line coordinates in the above are from (0.0,0.0) at the top left corner of the top left pixel
127 # to (width_in_pixels, height_in_pixels) at the bottom right corner of the bottom right pixel.
128 # The pixel/line location of the center of the top left pixel would therefore be (0.5,0.5)."
130 def gdal_info(filename):
131 """Returns similar information as the command line program gdalinfo. This can be used to assist adding
132 a new raster based source file to this library."""
133 dataset = osgeo.gdal.Open(filename, GA_ReadOnly)
134 print('raster size X: {} pixel'.format(dataset.RasterXSize))
135 print('raster size Y: {} pixel'.format(dataset.RasterYSize))
137 s_srs = osgeo.osr.SpatialReference()
138 s_srs.ImportFromWkt(dataset.GetProjectionRef())
139 t_srs = osgeo.osr.SpatialReference()
140 t_srs.ImportFromEPSG(4326)
141 # note that gdal_info reports lon/lat coordinates with the reference system t_srs = s_srs.CloneGeogCS()
142 # In general, this gives slightly different results than t_srs.ImportFromEPSG(4326) used here.
143 coord_trans = osgeo.osr.CoordinateTransformation(s_srs, t_srs)
145 gt = dataset.GetGeoTransform()
147 # create multipoint containing the four edges in lon/lat
148 multipoint = osgeo.ogr.Geometry(osgeo.ogr.wkbMultiPoint)
150 i = [0, dataset.RasterXSize][e % 2] # horizontal pixel
151 j = [0, dataset.RasterYSize][e // 2] # vertical pixel
152 x, y = osgeo.gdal.ApplyGeoTransform(gt, i, j) # map units (probably meter or something like that)
153 lon, lat, ele = coord_trans.TransformPoint(x, y)
155 point = osgeo.ogr.Geometry(osgeo.ogr.wkbPoint)
156 point.AddPoint(lon, lat)
157 multipoint.AddGeometry(point)
158 env = multipoint.GetEnvelope()
159 print('bounding box: east {}°, west {}°, south {}°, north {}°'.format(*env))
162 def gdal_xy_to_ele(x, y, filename, band=1):
163 """Returns the (linearly interpolated) elevation for the given point in the datasource specified by its filename.
165 :param x: horizontal value in the coordinate system specified by the datasource
166 :param y: vertical value in the coordinate system specified by the datasource
167 :param filename: filename of datasource
168 :param band: band of the datasource containing the elevation data
169 :return: Elevation as float or None
172 >>> gdal_xy_to_ele(76665, 241185, 'ibk_10m.asc')
175 dataset = osgeo.gdal.Open(filename, GA_ReadOnly)
176 assert dataset is not None
177 assert band <= dataset.RasterCount
178 band = dataset.GetRasterBand(band)
179 gt = dataset.GetGeoTransform() # gt == (xleft, xscale, xrot, ytop, yscale, yrot)
180 igt = osgeo.gdal.InvGeoTransform(gt)[1]
181 pixel, line = osgeo.gdal.ApplyGeoTransform(igt, x, y) # pixel and line are float values
182 if pixel < 0 or pixel >= dataset.RasterXSize or line < 0 or line >= dataset.RasterYSize:
183 return None # out of bounding box
185 # nearest pixel (spixel, sline) left & above of the position specified by (pixel, line)
186 # the 0.5 in the formula is because the center of pixels is in the middle of the pixels.
187 spixel, sline = int(pixel-0.5), int(line-0.5)
188 ele_str = band.ReadRaster(spixel, sline, 2, 2)
189 raster_format = {GDT_Int32: '=4i', GDT_Float32: '=4f'}[band.DataType]
190 ele_edges = struct.unpack(raster_format, ele_str) # [ele[spixel, sline], ele[spixel+1, sline], ele[spixel, sline+1], ele[spixel+1, sline+2]]
191 if band.GetNoDataValue() in ele_edges:
193 return interpolate_bilinear(pixel-0.5-spixel, line-0.5-sline, *ele_edges)
199 # The WMS 1.3 specification says about bounding boxes:
200 # The relation of the Bounding Box to the map pixel matrix is that the bounding box goes around the “outside”
201 # of the pixels of the map rather than through the centres of the map’s border pixels.
202 # In this context, individual pixels represent an area on the ground.
204 def wms_get_capabilities(url):
206 # url = 'http://vogis.cnv.at/mapserver/mapserv?map=i_hoehen_und_gelaende_r_wms.map&'
207 us = urllib.parse.urlsplit(url)
208 query = urllib.parse.parse_qs(us.query)
209 query.update({'VERSION': '1.0.3', 'REQUEST': 'GetCapabilities', 'SERVICE': 'WMS'})
210 gc_url = urllib.parse.SplitResult(us.scheme, us.netloc, us.path, urllib.parse.urlencode(query, doseq=True), us.fragment).geturl()
213 f = urllib.request.urlopen(gc_url)
218 wms_cap = et.fromstring(xml)
222 def wms_xy_to_qbbox(x, y, bbox, width, height):
223 """Returns the bounding box of the 4 pixel surrounding the given coordinates."""
224 x_idx = width * (x - bbox.minx) / (bbox.maxx - bbox.minx) - 0.5 # index of pixel of total map image in x direction (float)
225 y_idx = height * (y - bbox.miny) / (bbox.maxy - bbox.miny) - 0.5 # index of pixel of total map image in y direction (float)
230 qbbox = BBox(minx=x0_idx * (bbox.maxx - bbox.minx) / width + bbox.minx,
231 maxx=x1_idx * (bbox.maxx - bbox.minx) / width + bbox.minx,
232 miny=y0_idx * (bbox.maxy - bbox.miny) / height + bbox.miny,
233 maxy=y1_idx * (bbox.maxy - bbox.miny) / height + bbox.miny)
237 def wms_ij_to_ele(url, layer, crs, bbox, width, height, i, j):
238 """Returns the elevation via a getfeatureinfo request of the pixels specified by i and j within the range
239 specified by bbox, width and height."""
240 us = urllib.parse.urlsplit(url)
241 query = urllib.parse.parse_qs(us.query)
242 query.update({'VERSION': '1.3.0', 'REQUEST': 'GetFeatureInfo', 'LAYERS': layer, 'STYLES': '',
243 'CRS': crs, 'BBOX': ','.join(map(str, [bbox.minx, bbox.miny, bbox.maxx, bbox.maxy])),
244 'WIDTH': str(width), 'HEIGHT': str(height), 'FORMAT': 'image/png',
245 'QUERY_LAYERS': layer, 'INFO_FORMAT': 'application/vnd.ogc.gml', 'I': str(i), 'J': str(j)})
246 fi_url = urllib.parse.SplitResult(us.scheme, us.netloc, us.path, urllib.parse.urlencode(query, doseq=True), us.fragment).geturl()
249 f = urllib.request.urlopen(fi_url)
254 root = et.fromstring(xml)
255 return float(root.find('.//value_0').text)
258 def wms_xy_to_ele(x, y, url, layer, crs, bbox, width, height):
259 """Returns the (linearly interpolated) elevation for the given point from the WMS.
261 :param x: horizontal value in the native coordinate system of used layer of the WMS service
262 :param y: vertical value in the native coordinate system of used layer of the WMS service
264 :param layer: WMS layer name
265 :param crs: native coordinate reference system, e.g. 'EPSG:31254'
266 :param bbox: layer bounding box in native coordinate system of used layer of the WMS service (instance of BBox)
267 :param width: number of "pixel" in bbox in x direction
268 :param height: number of "pixel" in bbox in y direction
269 :return: Elevation as float or None
272 >>> wms_xy_to_ele(80000.0, 240000.0, url)
275 # crs=EPSG:31254&dpiMode=7&format=image/png&layers=hoehenmodell_2011_gelaende_50cm&styles=&url=http://vogis.cnv.at/mapserver/mapserv?map%3Di_hoehen_und_gelaende_r_wms.map%26version%3D1.3.0%26
277 # find qbbox (query bounding box) containing 4 pixels surrounding x/y and centers of 4 surrounding pixels.
278 # <BoundingBox CRS="EPSG:31254" minx="180000" miny="-75000" maxx="280000" maxy="0"/>
279 qbbox = wms_xy_to_qbbox(x, y, bbox, width, height)
282 for j in range(1, -1, -1):
284 'http://vogis.cnv.at/mapserver/mapserv?map=i_hoehen_und_gelaende_r_wms.map',
285 'hoehenmodell_2011_gelaende_50cm',
286 crs, qbbox, 2, 2, i, j)
291 minx = qbbox.minx + (qbbox.maxx-qbbox.minx)/4,
292 maxx = qbbox.minx + (qbbox.maxx-qbbox.minx)*3/4,
293 miny = qbbox.miny + (qbbox.maxy-qbbox.miny)/4,
294 maxy = qbbox.miny + (qbbox.maxy-qbbox.miny)*3/4) # this bbox has the edges at the pixel centers
295 return interpolate_bilinear((x-cbbox.minx)/(cbbox.maxx-cbbox.minx), (y-cbbox.miny)/(cbbox.maxy-cbbox.miny), *ele_list)
302 region = None # Human readable string shortly describing the covered region
303 source = None # Short description of data provider (string)
304 attribution = None # attribution string
305 resolution_m = None # (mean) distance between two samples in meter (float)
306 bbox = None # bounding box of data expressed in longitude/latitude (BBox)
307 epsg = None # native EPSG number, e.g. 31287 for 'EPSG:31287'
310 class MapSourceFile(MapSource):
311 filename = None # filename including path of map data
314 class MapSourceWmf(MapSource):
315 url = None # url of WMS service
316 layer = None # WMS layer Name
322 class MapSourceAustriaBasemap(MapSourceFile):
324 source = 'basemap.at'
326 bbox = BBox(9.314479, 17.358544, 46.297834, 49.022520)
327 filename = '/daten/GeoData/dem/oesterreich_10m/dhm_lamb_10m.tif'
331 def lonlat_to_ele_austria(lon, lat):
332 """DEM Austria 10x10m EPSG:31287
333 C-BY-3.0: Land Kaernten - data.ktn.gv.at"""
334 filename = '/daten/GeoData/dem/oesterreich_10m/dhm_lamb_10m.tif'
335 x, y = lonlat_to_epsg(lon, lat, 31287)
336 return gdal_xy_to_ele(x, y, filename)
342 class MapSourceWmfVorarlberg(MapSourceWmf):
343 region = 'Vorarlberg'
344 source = 'Landesamt fuer Vermessung und Geoinformation Vorarlberg'
346 bbox = BBox(9.33463, 10.333, 46.7549, 47.6586)
347 url = 'http://vogis.cnv.at/mapserver/mapserv?map=i_hoehen_und_gelaende_r_wms.map&'
348 layer = 'hoehenmodell_2011_oberflaeche_50cm'
352 def wms_getfeatureinfo_vbg(x, y):
353 """Uses a WMS GetFeatureInfo call to retrieve the DEM data.
355 >>> print wms_getfeatureinfo_vbg(9.783502, 47.335013)
357 # Note: a better (or at least alternative) way to implement this would be to use GDAL to do it for us.
359 # http://www.gdal.org/frmt_wms.html
360 # "Starting with GDAL >= 2.0, WMS layers can be queried (through a GetFeatureInfo request) with the
361 # gdallocationinfo utility, or with a GetMetadataItem("Pixel_iCol_iLine", "LocationInfo") call on a band object.
364 # filename='dem_vogis.xml'
365 # dataset = osgeo.gdal.Open(filename, GA_ReadOnly)
366 # assert dataset is not None
367 # assert band <= dataset.RasterCount
368 # band = dataset.GetRasterBand(1)
369 # band.GetMetadataItem("GeoPixel_{}_{}".format(1000, 1000), "LocationInfo")
370 # # No query is executed and None is returned.
372 # Using gdallocationinfo just returns the RGB values 255.
374 # The third alternative also just returns the RGB value:
375 # print dem.gdal_xy_to_ele(-100, 200000, 'dem_vogis.xml', 1)
384 ('map', 'i_hoehen_und_gelaende_r_wms.map'),
386 ('VERSION', '1.1.1'),
387 ('REQUEST', 'GetFeatureInfo'),
388 ('BBOX', ','.join([xl, yb, xr, yt])),
389 ('SRS', 'EPSG:4326'),
392 ('LAYERS', 'hoehenmodell_terrain_1m'),
393 ('QUERY_LAYERS', 'hoehenmodell_terrain_1m'),
394 ('INFO_FORMAT', 'gml'),
397 url = 'http://vogis.cnv.at/mapserver/mapserv?' + urllib.parse.urlencode(parameter)
398 f = urllib.request.urlopen(url)
401 root = et.fromstring(xml)
402 elevation = float(root.find('hoehenmodell_terrain_1m_layer/hoehenmodell_terrain_1m_feature/value_0').text)
406 def wrgeo_to_ele_vbg(wrgeo):
408 >>> wrgeo_to_ele_vbg('47.435870 N 10.085163 E')
411 x, y = wrgeo_to_lonlat(wrgeo)
412 return wms_getfeatureinfo_vbg(x, y)
418 # http://geoservices.buergernetz.bz.it/geoserver/p_bz-elevation/ows?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&BBOX=2922671.81412236206233501,1209060.08528055297210813,2926426.12536783283576369,1212054.986808244837448&CRS=EPSG:2056&WIDTH=1142&HEIGHT=911&LAYERS=DTM-2p5m&STYLES=&FORMAT=image/png&QUERY_LAYERS=DTM-2p5m&INFO_FORMAT=application/json&I=79&J=30
420 def utm32n_to_mapcode(utm_x, utm_y):
421 """The mapcode is the filename of the map part without extension.
423 >>> utm32n_to_mapcode(705033, 5175081)
427 # 604998.750 # left edge of 09013
428 # 608201.250 # right edge of 09013
429 # 608198.750 # left edge of 09012
430 # 611401.250 # right edge of 09012
431 # 611398.750 # left edge of 09023
432 # 614601.250 # right edge of 09023
433 east_i = int((utm_x-605000) // 3200 + 2)
434 east_major = east_i // 2
435 east_minor = east_i % 2
438 # 5128401.250 # upper edge of 02113
439 # 5125598.750 # lower edge of 02113
440 # 5125601.250 # upper edge of 01114
441 # 5122798.750 # lower edge of 01114
442 # 5122801.250 # upper edge of 01113
443 # 5119998.750 # lower edge of 01113
444 north_i = int((utm_y-5120000) // 2800 + 2)
445 north_major = north_i // 2
446 north_minor = north_i % 2
448 minor = {(1, 1): 1, (1, 0): 2, (0, 0): 3, (0, 1): 4}[(east_minor, north_minor)]
450 return 'dtm_utm{:02d}{:02d}{:d}'.format(north_major, east_major, minor)
453 def wrgeo_to_ele_stirol(wrgeo):
454 lon, lat = wrgeo_to_lonlat(wrgeo)
455 utm_x, utm_y = lonlat_to_utm32n(lon, lat)
456 mapcode = utm32n_to_mapcode(utm_x, utm_y)
458 # Unpack zip archive in /tmp
459 if not os.path.exists(os.path.join('/tmp', mapcode + '.ascii')):
460 shutil.copy(os.path.join('/daten/GeoData/dem/suedtirol', mapcode + '.zip'), '/tmp')
461 zip = zipfile.ZipFile(os.path.join('/tmp', mapcode + '.zip'), 'r')
462 zip.extractall('/tmp')
465 return gdal_xy_to_ele(utm_x, utm_y, os.path.join('/tmp', mapcode + '.ascii'))
471 def main(wrgeo, no_wms):
472 lon, lat = wrgeo_to_lonlat(wrgeo)
474 ele = lonlat_to_ele_austria(lon, lat)
476 ele_str = str(ele) if ele is None else '{:.0f} m'.format(ele)
477 print('lat: {:.6f}° lon: {:0.6f}° ele: {}'.format(lat, lon, ele_str))
480 if __name__ == '__main__':
481 parser = argparse.ArgumentParser('Get most accurate elevation for a given point from predefined GDAL data sources or WMS sources')
482 parser.add_argument('--no-wms', help='don\'t use WMF services')
483 parser.add_argument('wrgeo', help='coordinate like "47.275712 N 11.3456496 E"')
484 args = parser.parse_args()
485 main(args.wrgeo, args.no_wms)