Skip to content

Commit 90f21c6

Browse files
committed
Updated SRTM_clip_gui.py to use the OpenTopography Web API to fetch a
DEM. Updated GUI to require entering the center coordinates and then the width/height of the bounding box/square in meters. Same parameters and approach used as OpenAthena. Also added a command-line tool fetchdem.py that does the same thing. Then, used PyInstaller to pre-build for Windows, Linux/x86_64, MacOS ARM/x86_64. Updated README plus requirements because we need requirements python package. Python version needs an API KEY which is free from OpenTopography. The API KEY can be read from the environment variable OPEN_TOPOGRAPHY_API_KEY or entered in the gui or via command-line argument. Resulting DEMs have been tested with OA on android and ios.
1 parent aefb9cb commit 90f21c6

10 files changed

+439
-62
lines changed

README.md

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
# SRTM-clip-GUI
2-
A Graphical User Interface for downloading a terrain Digital Elevation Model (DEM) of a specified area on Earth ⛰ 🌎
32

4-
## About:
3+
A Graphical User Interface for downloading a terrain Digital Elevation
4+
Model (DEM) of a specified area on Earth ⛰ 🌎
55

6-
Output is saved in the GeoTIFF ".tif" file format.
6+
## About:
77

8-
This program uses the Python [elevation](https://pypi.org/project/elevation/) package internally, and data is sourced from the high-quality [SRTM 30m Global 1 arc second V003](https://lpdaac.usgs.gov/products/srtmgl1nv003/) dataset.
8+
Output is saved in the GeoTIFF ".tiff" file format.
99

10-
## Usage:
10+
This program uses the Python to interact with the [Open
11+
Topography](https://opentopography.org) website to download digital
12+
elavation model/map using their web API.
1113

12-
Enter the Minimum Latitude, Maximum Latitude, Minimum Longitude, and Maximum Longitude of the rectangular area you'd like to capture:
13-
<img width="768" alt="image of SRTM-clip-GUI running on MacOS. Default bounds 12.35 41.8 12.65 42 for Rome-30m-DEM.tif are already filled in" src="./assets/demo_main.png">
14+
Data is sourced from the high-quality Shuttle Radar Topography Mission
15+
[SRTM GL1](https://portal.opentopography.org/raster?opentopoID=OTSRTM.082015.4326.1)
16+
Global 30m dataset.
1417

18+
## Usage:
1519

16-
Click the "Clip" button, and your GeoTIFF DEM will be downloaded:
17-
<a href="https://github.com/mkrupczak3/OpenAthena#parsegeotiffpy"><img width="565" alt="Screenshot of a render of the Rome-30m-DEM.tif Digital Elevation Model file, using OpenAthena parseGeoTIFF.py on MacOS" src="./assets/Render_Rome-30m-DEM.png"></a>
20+
To generate a digital elevation model/map (DEM), enter the center
21+
latitude and longitude of the elevation map along with the
22+
width/height of the surrounding bounding box in meters. 15,000 meters
23+
squared is typical. An API key is needed to access the OpenTopography
24+
web API and is free to obtain by registering with the site. Depending
25+
on the platform, you may need to obtain an API key. You can cut/paste
26+
the API key into this field or set the environment variable
27+
OPENTOPOGRAPHY_API_KEY and it will be automatically read in at
28+
application start time. <img width="768" alt="image of SRTM-clip-GUI
29+
running on MacOS." src="./assets/demo_main_webapi.png">
30+
31+
Click the "Fetch" button and your GeoTIFF DEM will automatically be
32+
downloaded and saved into a file in the current directory. Filenames
33+
take the form "DEM_LatLon_s_w_n_e.tiff" where s, w, n, e, are the
34+
coordinates of the bounding box surrounding the center lat,lon.
1835

1936
## Install
20-
**TBD**
2137

2238
## Developer environment install:
2339

@@ -52,4 +68,5 @@ Use [PyInstaller](https://pyinstaller.org/en/stable/) to create a distributable
5268
pyinstaller -wF --collect-all elevation --icon ./assets/SRTM-cliptool-icon.ico SRTM_clip_gui.py
5369
```
5470

55-
(note that the distributable will be specific to your operating system and architecture, cross-compiling is not supported)
71+
(note that the distributable will be specific to your operating system
72+
and architecture, cross-compiling is not supported)

SRTM_clip_gui.exe

11.9 MB
Binary file not shown.

SRTM_clip_gui.linux

23 MB
Binary file not shown.

SRTM_clip_gui.macos-arm64

25.1 MB
Binary file not shown.

SRTM_clip_gui.macos-x86

11 MB
Binary file not shown.

SRTM_clip_gui.py

100644100755
Lines changed: 247 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,236 @@
1-
import elevation
1+
#!/usr/bin/env python3
2+
# SRTM_clip_gui.py
3+
# Matthew Krupczak, Bobby Krupczak
4+
# Theta Limited
5+
26
import PySimpleGUI as sg
37
import sys
48
import os
9+
import math
10+
import requests
11+
import getopt
12+
13+
lat = 0.0
14+
lon = 0.0
15+
n = 0.0
16+
s = 0.0
17+
w = 0.0
18+
e = 0.0
19+
diam = 15000
20+
21+
# to build a binary using pyinstaller, hardcode apiKeyStr
22+
apiKeyStr = ""
23+
24+
urlStr = "https://portal.opentopography.org/API/globaldem?"
25+
demTypeStr = "SRTMGL1"
26+
outputFormatStr = "GTiff"
27+
filenameSuffix = ".tiff"
28+
requestURLStr = ""
29+
verbose = False
30+
application_path = ""
31+
32+
# TODO: get separate API key, hard code, and then compile/build
33+
# windows/x86 app binary, mac/m1 binary, mac/x86 binary, and linux
34+
# x86 binary.
35+
# take out API key field from GUI and hardcode for the pre-compiled
36+
# versions, then put it back in for source code python git submission
37+
38+
# DEMs generated from this program successfully tested with
39+
# OA/python, OAiOS, OAandroid
40+
41+
# -------------------------------
42+
# given lat, lon of center, get bounding box
43+
44+
def getBoundingBox():
45+
46+
global n,s,e,w,diam
47+
48+
clat = lat * (math.pi / 180.0)
49+
clon = lon * (math.pi / 180.0)
50+
d = math.sqrt( 2.0 * (diam / 2.0) * (diam / 2.0) )
51+
52+
if verbose:
53+
print("Center lat,lon is ",clat,clon," within ",diam," x ",diam," box")
54+
55+
# go southwest X meters
56+
# SW = 225 degrees
57+
bearing = 225.0 * (math.pi / 180.0)
58+
arcLen = d / (6371 * 1000)
59+
60+
newLat = math.asin(math.sin(clat) * math.cos(arcLen) + math.cos(clat) * math.sin(arcLen) * math.cos(bearing) )
61+
newLon = clon + math.atan2(math.sin(bearing) * math.sin(arcLen) * math.cos(clat), math.cos(arcLen) - math.sin(clat) * math.sin(newLat))
62+
llLat = newLat * (180.0 / math.pi)
63+
llLon = newLon * (180.0 / math.pi)
64+
65+
# go northeast X meters
66+
# NE = 45
67+
bearing = 45.0 * (math.pi / 180.0)
68+
newLat = math.asin(math.sin(clat) * math.cos(arcLen) + math.cos(clat) * math.sin(arcLen) * math.cos(bearing) )
69+
newLon = clon + math.atan2(math.sin(bearing) * math.sin(arcLen) * math.cos(clat), math.cos(arcLen) - math.sin(clat) * math.sin(newLat))
70+
urLat = newLat * (180.0 / math.pi)
71+
urLon = newLon * (180.0 / math.pi)
72+
73+
# truncate to 6 decimal places XXX
74+
s = truncate(llLat,6)
75+
w = truncate(llLon,6)
76+
n = truncate(urLat,6)
77+
e = truncate(urLon,6)
78+
79+
if verbose:
80+
print("Bounding box is ",s,w,n,e)
81+
82+
return
83+
84+
# -------------------------------
85+
86+
def truncate(f, n):
87+
return math.floor(f * 10 ** n) / 10 **n
88+
89+
def textToInt(t):
90+
try:
91+
value = int(t)
92+
return value
93+
except:
94+
return math.inf
95+
96+
def textToFloat(t):
97+
try:
98+
value = float(t)
99+
return value
100+
except:
101+
return math.inf
102+
103+
# -------------------------------
104+
105+
def fetchDem():
106+
107+
global lat, lon, diam, apiKeyStr, window
108+
109+
# get params out of GUI and sanity check
110+
window['Results'].update("Going to fetch DEM")
5111

6-
# os.chdir(sys._MEIPASS)
112+
latText = values['lat']
113+
lonText = values['lon']
114+
diamText = values['diam']
115+
# when building an executable using pyinstaller, don't set apikeystr here
116+
# apiKeyStr = values['apiKey']
7117

118+
if latText == '' or lonText == '' or diamText == '' or apiKeyStr == '':
119+
window['Results'].update("Invalid parameters")
120+
return
8121

122+
lat = textToFloat(latText)
123+
lon = textToFloat(lonText)
124+
diam = textToInt(diamText)
125+
126+
if lat == math.inf or lon == math.inf or diam == math.inf:
127+
window['Results'].update("Invalid parameters")
128+
return
129+
130+
# get bounding box
131+
getBoundingBox()
132+
133+
# make request
134+
requestURLStr = urlStr + "demtype=" + demTypeStr + "&south=" + str(s) + "&north=" + str(n) + "&west=" + str(w) + "&east=" + str(e) + "&outputFormat=" + outputFormatStr + "&API_Key=" + apiKeyStr
135+
136+
137+
aStr = "Fetching DEM for "+str(lat)+","+str(lon)+" x "+str(diam)+" meters . . ."
138+
window['Results'].update(aStr)
139+
window.refresh()
140+
141+
if verbose:
142+
print("Request URL is ",requestURLStr)
143+
144+
# make the request and write file to DEM_LatLon_s_w_n_e.tiff
145+
response = requests.get(requestURLStr)
146+
147+
if response.status_code == 200:
148+
149+
# normally files written to directory with .py script
150+
# for windows pyinstaller version, we need to
151+
# set the directory explicitly
152+
153+
filename = "DEM_LatLon_"+str(s)+"_"+str(w)+"_"+str(n)+"_"+str(e)+filenameSuffix
154+
app_filename = os.path.join(application_path,filename)
155+
156+
f = open(app_filename,"wb+")
157+
f.write(response.content)
158+
f.close()
159+
160+
aStr = "Wrote " + str(len(response.content)) + " bytes to " + filename
161+
window['Results'].update(aStr)
162+
window.refresh()
163+
164+
if verbose:
165+
print(aStr)
166+
167+
else:
168+
if verbose:
169+
print("Request failed, error code ",response.status_code)
170+
171+
# no match statement prior to 3.10 and pyinstaller 4.x for windows
172+
# doesnt support match; so revert back to ifelse
173+
# match response.status_code:
174+
# case 204:
175+
# window['Results'].update("No elevation data for this lat,lon; try again after updating lat,lon")
176+
# case 400:
177+
# window['Results'].update("Bad request: bug in code perhaps?")
178+
# case 401:
179+
# aStr = "Unauthorized: check your API key:" + apiKeyStr
180+
# window['Results'].update(aStr)
181+
# case 403:
182+
# aStr = "Forbidden: check your API key:" + apiKeyStr
183+
# window['Results'].update(aStr)
184+
# case 404:
185+
# window['Results'].update("Not found: check the URL to see if API changed")
186+
# case 408:
187+
# window['Results'].update("Request timeout: are you connected to the InterWebs?")
188+
# case _:
189+
# aStr = "Failed to fetch DEM, error code is:" + str(response.status_code)
190+
# window['Results'].update(aStr)
191+
192+
rcode = response.status_code
193+
if rcode == 204:
194+
window['Results'].update("No elevation data for this lat,lon; try again after updating lat,lon")
195+
elif rcode == 400:
196+
window['Results'].update("Bad request: bug in code perhaps?")
197+
elif rcode == 401:
198+
aStr = "Unauthorized: check your API key:" + apiKeyStr
199+
window['Results'].update(aStr)
200+
elif rcode == 403:
201+
aStr = "Forbidden: check your API key: XXX"
202+
window['Results'].update(aStr)
203+
elif rcode == 404:
204+
window['Results'].update("Not found: check the URL to see if API changed")
205+
elif rcode == 408:
206+
window['Results'].update("Request timeout: are you connected to the InterWebs?")
207+
else:
208+
aStr = "Failed to fetch DEM, error code is:" + str(response.status_code)
209+
window['Results'].update(aStr)
210+
211+
# update results with error code or
212+
# number of bytes written to filename
213+
# window['Results'].update("Finished, result is X")
214+
215+
return
216+
217+
# -------------------------------
218+
219+
def usage():
220+
print(sys.argv[0]," [-v] [-h]")
221+
sys.exit(0)
222+
223+
# -------------------------------
224+
# main
225+
# check if -v flag set for verbose
226+
227+
opts, args = getopt.getopt(sys.argv[1:],"vh")
228+
for o, v in opts:
229+
if o == "-v":
230+
verbose = True
231+
if o == "-h":
232+
usage()
233+
9234
if hasattr(sys, '_MEIPASS'):
10235
# PyInstaller >= 1.6
11236
os.chdir(sys._MEIPASS)
@@ -17,66 +242,38 @@
17242
else:
18243
pass
19244

20-
font=(sg.DEFAULT_FONT, 16)
245+
if getattr(sys,'frozen', False):
246+
application_path = os.path.dirname(sys.executable)
247+
elif __file__:
248+
application_path = os.path.dirname(__file__)
21249

22-
elevation.clean() # Clear cache files, in case they are stale or corrupted
23-
def clip(out_file, bounds):
24-
# Parse bounds into a tuple of floats
25-
clipped_data = elevation.clip(output=out_file, bounds=bounds)
26250

27-
sg.popup('Clip successful!', font=font)
251+
# try to get API key from environment variable
252+
# when building an executable using pyinstaller, don't pull apikey
253+
# from environment var
28254

255+
apiKeyStr = os.getenv("OPENTOPOGRAPHY_API_KEY","")
29256

257+
font=(sg.DEFAULT_FONT, 16)
30258

31259
layout = [
32-
[sg.Text('Output file:', font=font), sg.InputText(font=font), sg.FileSaveAs(font=font), sg.Text('Will be saved as: ".tif" file', font=font)],
33-
[sg.Text('Bounds', font=font)],
34-
[sg.Text('Minimum Latitude:', font=font), sg.InputText('12.35', font=font), sg.Text('Maximum Latitude:', font=font), sg.InputText('12.65', font=font)],
35-
[sg.Text('Minimum Longitude:', font=font), sg.InputText('41.8', font=font), sg.Text('Maximum Longitude:', font=font), sg.InputText('42', font=font)],
36-
[sg.Button('Clip', font=font)]
260+
261+
# when building an executable using pyinstaller, don't show the api key
262+
[sg.Text('API Key:', font=font), sg.InputText(apiKeyStr, font=font, key='apiKey', tooltip='Register to get your free OpenTopography API key from https://portal.opentopography.org\nPut that API key here or in OPENTOPOGRAPHY_API_KEY environment variable.')],
263+
[sg.Text('Center Latitude:', font=font), sg.InputText(' 0.0', font=font, key='lat', tooltip='Latitude in decimal format')],
264+
[sg.Text('Center Longitude:', font=font), sg.InputText(' 0.0', font=font, key='lon', tooltip='Longitude in decimal format')],
265+
[sg.Text('Height/width (m):', font=font), sg.InputText(' 15000', font=font, key='diam', tooltip='Height/width of the surrounding bounding box in meters.')],
266+
[sg.Text('Results: Output file will be DEM_LatLon_xxx', font=font, key='Results')],
267+
[sg.Button('Fetch', font=font)]
37268
]
38269

39-
window = sg.Window('Elevation Clip', layout)
270+
window = sg.Window('Fetch Digital Elevation Map from OpenTopography', layout)
40271

41272
while True:
42273
event, values = window.read()
43274
if event in (sg.WIN_CLOSED, 'Exit'):
44275
break
45-
if event == 'Clip':
46-
out_file = values[0]
47-
basename = '.'.join(values[0].split('.')[0:])
48-
ext = values[0].split('.')[-1].lower()
49-
if out_file != ext:
50-
out_file = basename + '.tif'
51-
else:
52-
out_file = out_file + '.tif'
53-
54-
print(f'filename: {out_file}')
55-
print(f'basename: {basename}')
56-
57-
try:
58-
min_lat = float(values[1])
59-
max_lat = float(values[2])
60-
min_lon = float(values[3])
61-
max_lon = float(values[4])
62-
except ValueError:
63-
sg.popup('ERROR: input was not a number!', font=font)
64-
continue
65-
if min_lat >= max_lat:
66-
sg.popup('ERROR: Minimum Latitude >= Maximum Latitude', font=font)
67-
continue
68-
if min_lon >= max_lon:
69-
sg.popup('ERROR: Minimum Longitude >= Maximum Longitude', font=font)
70-
continue
71-
for val in [min_lat, max_lat]:
72-
if val > 90.0 or val < -90.0:
73-
sg.popup('ERROR: Latitude was out of range!', font=font)
74-
continue
75-
for val in [min_lon, max_lon]:
76-
if val > 180.0 or val <= -180.0:
77-
sg.popup('ERROR: Longitude was out of range!', font=font)
78-
continue
79-
80-
clip(out_file, (min_lat, min_lon, max_lat, max_lon))
276+
if event == 'Fetch':
277+
fetchDem()
81278

82279
window.close()

assets/demo_main_webapi.png

46.5 KB
Loading
41.9 KB
Loading

0 commit comments

Comments
 (0)