-
Notifications
You must be signed in to change notification settings - Fork 0
/
ImportSchweizmobil.py
382 lines (322 loc) · 13.5 KB
/
ImportSchweizmobil.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
"""
This python script
- authenticates to schweizmobil.ch with a given username/password
- obtains the list of tracks
- per track finds the detail info, e.g. all coordinates and metadata
- assembles a GeoJSON Feature Collection object containing all tracks
- stores the assembled object
BUGS: - error handling
- what if files can't be written?
- what if format of cache files is "wrong"
Joerg Kummer 10. Oct 2023
"""
import requests
import json
import pyproj
from os.path import isfile,exists,expanduser
from datetime import datetime, timedelta
import PySimpleGUI as sg
# The following variables are made global and are set prior to
# exececuting ImportSchweizmobil()
# Because of this, the importer can overwrite these values
# and the values defined here become defaults
Filter={
"hike":True, "bike":True,
"MinDuration":0, "MaxDuration":1000,
"MinLength":0, "MaxLength":500,
"MinUp":0, "MaxUp":5000,
"Nincludes":"", "Nexcludes":"",
"MinLon":5.7, "MaxLon":10.8,
"MinLat":45.7, "MaxLat":48.0,
"MinMdate":'01/01/1970', "MaxMdate":'01/01/2050',
"MinUdate":'01/01/1970', "MaxUdate":'01/01/2050',
"id":""
}
# debug levels
# 0 - no console outputs
# 1 - filenames, API calls
# 2 - details on certain params
# 3 - full GeoJSON output; include 20 tracks only
debug=1
# folder for the output files
outfp=expanduser('~')+'/Documents/python/TrackMapper/'
# defines type of features in the output file:
# 0 - rectangles covering the via points of each track
# 1 - the via points of the track
# 2 - the full track coordinates (without elevation data)
opo=2
# This is the schweizmobil.ch API prefix
pre= 'https://map.schweizmobil.ch/api/'
# username / password for schweizmobil.ch
creds = json.dumps({
"username": "username goes in here",
"password": "password goes in here"
})
def Import_Schweizmobil():
# The following variables are made global and are set inside
# ImportSchweizmobil()
# Because of this, the importer can not incluence what values
# this module will use, but the importer can read the values
# after invoking ImportSchweizmobil()
global SchweizmobCacheDir
SchweizmobCacheDir='cache/schweizmobil.ch/'
global trksfn
trksfn=outfp+SchweizmobCacheDir+"tracks response.geojson"
global outfn
outfn=outfp+"GeoJSON/schweizmobil.GeoJSON"
# init requests object
session = requests.Session()
session.headers={}
try:
#login
response = session.post(pre+'4/login',data=creds)
rst=response.status_code
ErrC=response.json()['loginErrorCode']
if (rst!=200 or ErrC!=200):
raise Exception (f"Authentication failed; ({rst}/{ErrC})")
# fetch the list of tracks
response = session.get(pre+'5/tracks')
if debug>1: print ("Tracks API call ",response.status_code)
if response.status_code==200:
tracks=response.json()
# API was reachable, so let's write the response to a file, in case the API is
# unreachable the next time around
fi=open(trksfn,mode="w")
print(response.text,file=fi)
fi.close()
if debug>0: print ("Tracks API response cached",trksfn)
else:
raise Exception(f"Status Code {response.status_code}")
except Exception as e:
print (e)
print("Issues talking to schweizmobil.ch! Credentials? Offline ? Wrong API prefix?")
try:
print ("Trying to continue based on response cached earlier")
fi=open(trksfn,mode="r")
tracks=json.load(fi)
fi.close()
except:
print ("No cache file - giving up !")
return
# setup progress bar
# layout the form
layout = [[sg.Text(f'Processing {len(tracks)} tracks')],
[sg.ProgressBar(max_value=len(tracks), orientation='h', size=(20, 20), key='progress')]]
window = sg.Window('Progress', layout, finalize=True)
progress_bar = window['progress']
# setup transformer object to transform
# Swiss LV03 coordinates used by schweizmobil.ch to WGS84
trafo=pyproj.Transformer.from_crs(21781,4326)
# start building the GeoJSON FeatureCollection
geo={'type':'FeatureCollection',
'features':[]
}
k=0
included=0
# process each track in the tracks reponse
if debug>0: print("Start fetching track details")
for i in tracks:
if ((debug>1) and (k==20)): break
k=k+1
progress_bar.update_bar(k)
iid=i['id']
ts=i['modified_at'].replace(":", "" )
trkn=f'{outfp}{SchweizmobCacheDir}track {iid}-{ts}.geojson'
# write a cache file, if not present
# to offload the API and speed up processing
# result is the "track" dict representing the GeoJSON provided
# by the schweizmobil.ch
if not isfile(trkn):
# API call to fetch the track detail
response = session.get(pre+'4/tracks/'+str(iid))
if response.status_code!=200:
print (f'API call to obtain details on track {iid}\
failed with status {response.status_code}')
return
if debug>1: print (f"Track API call for {iid} successful")
# prepare the dict "track" from response
track=response.json()
fi=open(trkn,mode="w")
print (response.text,file=fi)
fi.close()
if debug>0: print ("wrote track cache ", trkn)
else: # read from cache file
fi=open(trkn,mode="r")
# make dict track from file
track=json.load(fi)
fi.close()
if debug>1:print ("read track cache ", trkn)
# reformat properties
# handling exceptions here as the format/availability of the
# properties is up to schweizmobil.ch and may change
try:
props=track["properties"]
fmt='%Y-%m-%dT%H:%M:%SZ'
mstmp=datetime.strptime(i['modified_at'],'%Y-%m-%dT%H:%M:%SZ')
datestr=mstmp.strftime("%d. %b %Y")
if (props['userdate']!="null" and props['userdate']!=None):
ustmp=datetime.strptime(props['userdate'],'%Y-%m-%d')
ud=ustmp.strftime("%d. %b %Y")
# Winter hike and snowshoe hike unhandled for the time being
if i['timetype']=="wander":
tt="hike"
minutes=round(props['meta']['walking'])
ttime=str(timedelta(minutes=minutes))
else:
tt="bike"
minutes=round(props['meta']['biking'])
ttime=str(timedelta(minutes=minutes))
# newprops will go into the new GeoJSON feature we are building
newprops={
'id':iid,
'Name':i['name'],
'URL': f'https://map.schweizmobil.ch/?trackId={iid}',
'User date':ud,
'Modified':i['modified_at'],
'Track modified':datestr,
'Track type':tt,
'Duration':ttime,
'Length':str(round(props['meta']['length'])/1000)+" km",
'Total up':str(round(props['meta']['totalup']))+" m",
'Total down':str(round(props['meta']['totaldown']))+" m",
'Min elevation':str(round(props['meta']['elemin']))+" m",
'Max elevation':str(round(props['meta']['elemax']))+" m"
}
except Exception as e:
if debug>0: print (f'Track {iid}: properties incomplete - skipping')
print(str(e))
continue
# Check against Filter values related to the properties (not the coordinates)
if (minutes<int(Filter["MinDuration"]) or minutes>int(Filter["MaxDuration"])):
if debug>1: print (f"skipping track {iid} because of duration filter")
continue
x=round(props['meta']['length'])/1000
if (x<float(Filter["MinLength"]) or x>float(Filter["MaxLength"])):
if debug>1: print (f"skipping track {iid} because of length filter")
continue
x=round(props['meta']['totalup'])
if (x<float(Filter["MinUp"]) or x>float(Filter["MaxUp"])):
if debug>1: print (f"skipping track {iid} because of Total up filter")
continue
if Filter["Nincludes"]:
if not Filter["Nincludes"].lower() in i["name"].lower():
if debug>1: print (f"skipping track {iid} because of Name includes filter")
continue
if Filter["Nexcludes"]:
if Filter["Nexcludes"].lower() in i["name"].lower():
if debug>1: print (f"skipping track {iid} because of Name excludes filter")
continue
mminf=datetime.strptime(Filter["MinMdate"],'%d/%m/%Y')
mmaxf=datetime.strptime(Filter["MaxMdate"],'%d/%m/%Y')
if mstmp<mminf or mstmp>mmaxf:
if debug>1:
print (f"skipping track {iid} because of modified date filter")
continue
if (props['userdate']!="null" and props['userdate']!=None):
mminf=datetime.strptime(Filter["MinUdate"],'%d/%m/%Y')
mmaxf=datetime.strptime(Filter["MaxUdate"],'%d/%m/%Y')
if ustmp<mminf or ustmp>mmaxf:
if debug>1:
print (f"skipping track {iid} because of user date filter")
continue
if Filter["id"]:
if str(i["id"])!=Filter["id"]:
if debug>1: print (f"skipping track {iid} because of id filter")
continue
if (i['timetype']=="wander") and (not Filter["hike"]):
if debug>1: print (f"skipping track {iid} because of track type filter")
continue
if (i['timetype']=="velo") and (not Filter["bike"]):
if debug>1: print (f"skipping track {iid} because of track type filter")
continue
# transform/reformat coordinates, find enclosing rectangle etc
# finish constructing the feature dict representing the track
# skip this track if the format does not contain the
# expected properties.via_points dict
try:
via=json.loads(props["via_points"])
except:
if debug>0: print (f'Track {iid}: no via data - skipping')
continue
# find the coordinates of the rectangle enclosing
# the via max/min values
# via points are the points selected by the schweizmobil.ch user
# when drawing the track. The other coordinates are extrapolated
# by schweizmobil.ch
latmax=0; lonmax=0
latmin=1000000; lonmin=1000000
for j in via:
(j[1],j[0])=trafo.transform(j[0],j[1])
latmax=max(latmax,j[1])
latmin=min(latmin,j[1])
lonmax=max(lonmax,j[0])
lonmin=min(lonmin,j[0])
# skip tracks if they are outside the coordinate filter
if (lonmin>float(Filter["MaxLon"]) or lonmax<float(Filter["MinLon"])):
if debug>1: print (f"skipping track {iid} because of longitude filter")
continue
if (latmin>float(Filter["MaxLat"]) or latmax<float(Filter["MinLat"])):
if debug>1:
print (f"skipping track {iid} because of latitude filter")
continue
# prepare feature structure
if opo==0:
feature={
'type':'Feature',
'geometry': {
'type':'Polygon',
'coordinates':[[
[lonmax,latmax],
[lonmax,latmin],
[lonmin,latmin],
[lonmin,latmax],
[lonmax,latmax]
]]
},
'properties': newprops
}
if opo==1:
feature={
'type':'Feature',
'geometry': {
'type': 'LineString',
'coordinates':via
},
'properties': newprops
}
if opo==2:
try:
coord=track['geometry']['coordinates']
for j in coord:
(j[1],j[0])=trafo.transform(j[0],j[1])
except:
if debug>0: print (f'Track {iid}: no Geometry.Coordinates data - skipping')
continue
feature={
'type':'Feature',
'geometry': track['geometry'],
'properties': newprops
}
# append the feature dict (current track) to the geo dict (collection of tracks)
if (opo==0 or opo==1 or opo==2):
geo['features'].append(feature)
included+=1
# close the progress bar window
window.close()
if geo['features']!=[]:
if debug>0:
match opo:
case 0: print ("Output includes only a bounding box")
case 1: print ("Output includes via coordinates")
case 2: print ("Output includes full track coordinates")
case _: print ("Unsupported opo parameter value - no track/features written")
print (f"Included {included} of {len(tracks)} tracks")
f=open(f'{outfn}',mode="w")
if debug<2:
print (json.dumps(geo),file=f)
else:
print (json.dumps(geo, indent="\t"),file=f)
f.close()
if debug>0: print(f"GeoJSON with schweizmobil.ch tracks written: {outfn}")
else:
print("\nAfter filtering there were no more tracks, map not updated!\n")