mirix commited on
Commit
43b69a7
·
verified ·
1 Parent(s): 683ee80

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -437
app.py CHANGED
@@ -1,49 +1,99 @@
1
- import io
2
- import json
3
- import pytz
4
- import numpy as np
5
  import pandas as pd
6
- pd.options.mode.chained_assignment = None
7
- from geopy import distance
8
- import plotly.graph_objects as go
9
- import base64
10
- import gpxpy
11
- from gpx_converter import Converter
12
- from sunrisesunset import SunriseSunset
13
- from datetime import datetime, date, timedelta
14
- from beaufort_scale.beaufort_scale import beaufort_scale_kmh
15
- from timezonefinder import TimezoneFinder
16
- tf = TimezoneFinder()
17
-
18
- from dash import Dash, dcc, html, dash_table, Input, Output, State, no_update, callback, _dash_renderer
19
- import dash_bootstrap_components as dbc
20
- import dash_mantine_components as dmc
21
- _dash_renderer._set_react_version('18.2.0')
22
-
23
- from dash_extensions import Purify
24
 
25
  import srtm
26
  elevation_data = srtm.get_data()
27
 
 
 
 
 
 
 
28
  import requests_cache
29
  import openmeteo_requests
30
  from retry_requests import retry
31
 
32
- ### VARIABLES ###
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- hdate_object = date.today()
35
- hour = '10'
36
- minute = '30'
37
- speed = 4.0
38
- frequency = 2
 
 
 
 
 
 
 
39
 
40
- # Variables to become widgets
41
- igpx = 'default_gpx.gpx'
42
- hdate = hdate_object.strftime('%Y-%m-%d')
43
- time = hour + ':' + minute
44
- granularity = frequency * 1000
45
 
46
- # Setup the Open Meteo API client with cache and retry on error
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
48
  retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
49
  openmeteo = openmeteo_requests.Client(session = retry_session)
@@ -52,8 +102,7 @@ openmeteo = openmeteo_requests.Client(session = retry_session)
52
  url = 'https://api.open-meteo.com/v1/forecast'
53
  params = {
54
  'timezone': 'auto',
55
- 'minutely_15': ['temperature_2m', 'rain', 'wind_speed_10m', 'weather_code', 'is_day'],
56
- 'hourly': ['rain'],
57
  }
58
 
59
  # Load the JSON files mapping weather codes to descriptions and icons
@@ -62,33 +111,7 @@ with open('weather_icons_custom.json', 'r') as file:
62
 
63
  # Weather icons URL
64
  icon_url = 'https://raw.githubusercontent.com/basmilius/weather-icons/refs/heads/dev/production/fill/svg/'
65
- sunrise_icon = icon_url + 'sunrise.svg'
66
- sunset_icon = icon_url + 'sunset.svg'
67
-
68
- ### FUNCTIONS ###
69
-
70
- # Sunrise sunset
71
- def sunrise_sunset(lat_start, lon_start, lat_end, lon_end, hdate):
72
-
73
- tz = tf.timezone_at(lng=lon_start, lat=lat_start)
74
- zone = pytz.timezone(tz)
75
-
76
- day = datetime.strptime(hdate, '%Y-%m-%d')
77
 
78
- dt = day.astimezone(zone)
79
-
80
- rs_start = SunriseSunset(dt, lat=lat_start, lon=lon_start, zenith='official')
81
- rise_time = rs_start.sun_rise_set[0]
82
-
83
- rs_end = SunriseSunset(dt, lat=lat_end, lon=lon_end, zenith='official')
84
- set_time = rs_end.sun_rise_set[1]
85
-
86
- sunrise = rise_time.strftime('%H:%M')
87
- sunset = set_time.strftime('%H:%M')
88
-
89
- return sunrise, sunset
90
-
91
- # Map weather codes to descriptions and icons
92
  def map_icons(df):
93
  code = df['weather_code']
94
 
@@ -122,44 +145,20 @@ def rain_intensity(precipt):
122
  rain = 'No rain / No info'
123
  return rain
124
 
125
- # Function to add elevation
126
- def add_ele(row):
127
- if pd.isnull(row['altitude']):
128
- row['altitude'] = elevation_data.get_elevation(row['latitude'], row['longitude'], 0)
129
- else:
130
- row['altitude'] = row['altitude']
131
- return row
132
-
133
- # Compute distances using the Karney algorith with Euclidian altitude correction
134
- def eukarney(lat1, lon1, alt1, lat2, lon2, alt2):
135
- p1 = (lat1, lon1)
136
- p2 = (lat2, lon2)
137
- karney = distance.distance(p1, p2).m
138
- return np.sqrt(karney**2 + (alt2 - alt1)**2)
139
-
140
  # Obtain the weather forecast for each waypoint at each specific time
141
- def get_weather(df_wp):
142
-
143
- params['latitude'] = df_wp['latitude']
144
- params['longitude'] = df_wp['longitude']
145
- params['elevation'] = df_wp['altitude']
146
 
147
- start_dt = datetime.strptime(hdate + 'T' + time, '%Y-%m-%dT%H:%M')
 
 
148
 
149
- delta_dt = start_dt + timedelta(seconds=df_wp['seconds'])
150
- delta_read = delta_dt.strftime('%Y-%m-%dT%H:%M')
151
 
152
- start_period = (delta_dt - timedelta(seconds=1800)).strftime('%Y-%m-%dT%H:%M')
153
- end_period = (delta_dt + timedelta(seconds=1800)).strftime('%Y-%m-%dT%H:%M')
154
 
155
- time_read = delta_dt.strftime('%H:%M')
156
-
157
- df_wp['Time'] = time_read
158
-
159
- params['start_minutely_15'] = delta_read
160
- params['end_minutely_15'] = delta_read
161
- params['start_hour'] = delta_read
162
- params['end_hour'] = delta_read
163
 
164
  responses = openmeteo.weather_api(url, params=params)
165
 
@@ -167,397 +166,141 @@ def get_weather(df_wp):
167
  response = responses[0]
168
 
169
  # Process hourly data. The order of variables needs to be the same as requested.
170
- minutely = response.Minutely15()
171
  hourly = response.Hourly()
172
 
173
- minutely_temperature_2m = minutely.Variables(0).ValuesAsNumpy()[0]
174
- rain = hourly.Variables(0).ValuesAsNumpy()[0]
175
- minutely_wind_speed_10m = minutely.Variables(2).ValuesAsNumpy()[0]
176
- weather_code = minutely.Variables(3).ValuesAsNumpy()[0]
177
- is_day = minutely.Variables(4).ValuesAsNumpy()[0]
 
178
 
179
- df_wp['Temp (°C)'] = minutely_temperature_2m
180
- df_wp['weather_code'] = weather_code
181
- df_wp['is_day'] = is_day
182
 
183
  v_rain_intensity = np.vectorize(rain_intensity)
184
- df_wp['Rain level'] = v_rain_intensity(rain)
185
 
186
  v_beaufort_scale_kmh = np.vectorize(beaufort_scale_kmh)
187
 
188
- df_wp['Wind level'] = v_beaufort_scale_kmh(minutely_wind_speed_10m, language='en')
189
-
190
- df_wp['Rain (mm/h)'] = rain.round(1)
191
- df_wp['Wind (km/h)'] = minutely_wind_speed_10m.round(1)
192
-
193
- return df_wp
194
-
195
- # Parse the GPX track
196
- def parse_gpx(df_gpx, hdate):
197
-
198
- # Sunrise sunset
199
-
200
- lat_start, lon_start = df_gpx[['latitude', 'longitude']].head(1).values.flatten().tolist()
201
- lat_end, lon_end = df_gpx[['latitude', 'longitude']].tail(1).values.flatten().tolist()
202
-
203
- sunrise, sunset = sunrise_sunset(lat_start, lon_start, lat_end, lon_end, hdate)
204
 
205
- df_gpx = df_gpx.apply(lambda x: add_ele(x), axis=1)
 
206
 
207
- centre_lat = (df_gpx['latitude'].max() + df_gpx['latitude'].min()) / 2
208
- centre_lon = (df_gpx['longitude'].max() + df_gpx['longitude'].min()) / 2
209
 
210
- # Create shifted columns in order to facilitate distance calculation
211
 
212
- df_gpx['lat_shift'] = df_gpx['latitude'].shift(periods=-1).fillna(df_gpx['latitude'])
213
- df_gpx['lon_shift'] = df_gpx['longitude'].shift(periods=-1).fillna(df_gpx['longitude'])
214
- df_gpx['alt_shift'] = df_gpx['altitude'].shift(periods=-1).fillna(df_gpx['altitude'])
215
 
216
- # Apply the distance function to the dataframe
217
 
218
- df_gpx['distances'] = df_gpx.apply(lambda x: eukarney(x['latitude'], x['longitude'], x['altitude'], x['lat_shift'], x['lon_shift'], x['alt_shift']), axis=1).fillna(0)
219
- df_gpx['distance'] = df_gpx['distances'].cumsum().round(decimals = 0).astype(int)
220
 
221
- df_gpx = df_gpx.drop(columns=['lat_shift', 'lon_shift', 'alt_shift', 'distances']).copy()
 
 
222
 
223
- start = df_gpx['distance'].min()
224
- finish = df_gpx['distance'].max()
225
 
226
- dist_rang = list(range(start, finish, granularity))
227
- dist_rang.append(finish)
 
 
228
 
229
- way_list = []
230
- for waypoint in dist_rang:
231
- gpx_dict = df_gpx.iloc[(df_gpx.distance - waypoint).abs().argsort()[:1]].to_dict('records')[0]
232
- way_list.append(gpx_dict)
233
 
234
- df_wp = pd.DataFrame(way_list)
 
 
 
 
 
 
235
 
236
- df_wp['seconds'] = df_wp['distance'].apply(lambda x: int(round(x / (speed * (5/18)), 0)))
 
237
 
238
- df_wp = df_wp.apply(lambda x: get_weather(x), axis=1)
 
239
 
240
- df_wp['Temp (°C)'] = df_wp['Temp (°C)'].round(0).astype(int).astype(str) + '°C'
241
- df_wp['is_day'] = df_wp['is_day'].astype(int)
242
- df_wp['weather_code'] = df_wp['weather_code'].astype(int)
243
- df_wp = df_wp.apply(map_icons, axis=1)
244
 
245
- df_wp['Rain level'] = df_wp['Rain level'].astype(str)
246
- df_wp['Wind level'] = df_wp['Wind level'].astype(str)
 
 
 
 
 
 
247
 
248
- df_wp['dist_read'] = ('<p style="font-family:sans; font-size:12px;"><b>' +
249
- df_wp['Weather outline'] + '</b><br><br>' +
250
- df_wp['Temp (°C)'] + '<br><br>' +
251
- df_wp['Rain level'] + '<br>' +
252
- df_wp['Wind level'] + '<br><br>' +
253
- df_wp['Time'] + '<br><br>' +
254
- df_wp['distance'].apply(lambda x: str(int(round(x / 1000, 0)))).astype(str) + ' km | ' + df_wp['altitude'].round(0).astype(int).astype(str) + ' m</p>')
255
 
256
- df_wp = df_wp.reset_index(drop=True)
257
 
258
- df_wp['Waypoint'] = df_wp.index
259
-
260
- dfs = df_wp[['Waypoint', 'Time', 'Weather', 'Weather outline', 'Temp (°C)', 'Rain (mm/h)', 'Rain level', 'Wind (km/h)', 'Wind level']].copy()
 
 
 
261
 
262
- dfs['Wind (km/h)'] = dfs['Wind (km/h)'].round(1).astype(str).replace('0.0', '')
263
- dfs['Rain (mm/h)'] = dfs['Rain (mm/h)'].round(1).astype(str).replace('0.0', '')
264
- dfs['Temp (°C)'] = dfs['Temp (°C)'].str.replace('C', '')
265
 
266
- dfs['Weather'] = '<img style="float: right; padding: 0; margin: -6px; display: block;" width=48px; src=' + dfs['Weather'] + '>'
267
 
268
- return df_gpx, df_wp, dfs, sunrise, sunset, centre_lat, centre_lon
 
269
 
270
- ### PLOTS ###
271
 
272
- # Plot map
273
 
274
- def plot_fig(df_gpx, df_wp, centre_lat, centre_lon):
275
 
276
  fig = go.Figure()
277
 
278
- fig.add_trace(go.Scattermap(lon=df_gpx['longitude'],
279
- lat=df_gpx['latitude'],
280
- mode='lines', line=dict(width=4, color='firebrick'),
281
- name='gpx_trace'))
282
 
283
- fig.add_trace(go.Scattermap(lon=df_wp['longitude'],
284
- lat=df_wp['latitude'],
285
- mode='markers+text', marker=dict(size=24, color='firebrick', opacity=0.8, symbol='circle'),
286
- textfont=dict(color='white', weight='bold'),
287
- text=df_wp.index.astype(str),
288
- name='wp_trace'))
289
 
290
  fig.update_layout(map_style='open-street-map',
291
- map=dict(center=dict(lat=centre_lat, lon=centre_lon), zoom=12))
292
 
293
- fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'wp_trace'}))
294
- fig.update_traces(showlegend=False, hoverinfo='skip', hovertemplate=None, selector=({'name': 'gpx_trace'}))
295
 
296
  return fig
297
 
298
- ### DASH APP ###
299
-
300
- external_stylesheets = [dbc.themes.BOOTSTRAP, dmc.styles.ALL]
301
-
302
- app = Dash(__name__, external_stylesheets=external_stylesheets)
303
-
304
  server = app.server
305
 
306
- # Layout
307
-
308
- hours = [str(n).zfill(2) for n in range(0, 24)]
309
- minutes = [str(n).zfill(2) for n in range(0, 60, 5)]
310
-
311
- picker_style = {
312
- 'display': 'inline-block',
313
- 'width': '35px',
314
- 'height': '32px',
315
- 'cursor': 'pointer',
316
- 'border': 'none',
317
- }
318
-
319
  def serve_layout():
320
 
321
  layout = html.Div([
322
- html.Div([dcc.Link('The Weather for Hikers', href='.',
323
- style={'color': 'darkslategray', 'font-size': 18, 'font-family': 'sans', 'font-weight': 'bold', 'text-decoration': 'none'}),
324
- ]),
325
- html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/',
326
- target='_blank', style={'color': 'goldenrod', 'font-size': 14, 'font-family': 'sans', 'text-decoration': 'none'}),
327
- ]),
328
- html.Div([html.Br(),
329
- dbc.Row([
330
- dbc.Col([dcc.Upload(id='upload-gpx', children=html.Div(id='name-gpx'),
331
- accept='.gpx, .GPX', max_size=10000000, min_size=100,
332
- style={
333
- 'width': '174px',
334
- 'height': '48px',
335
- 'lineWidth': '174px',
336
- 'lineHeight': '48px',
337
- 'borderWidth': '2px',
338
- 'borderStyle': 'solid',
339
- 'borderColor': 'goldenrod',
340
- 'textAlign': 'center',
341
- },
342
- ), dcc.Store(id='store-gpx')], width={'size': 'auto', 'offset': 1}),
343
- dbc.Col([dmc.Divider(orientation='vertical', size=2, color='goldenrod', style={'height': 82})], width={'size': 'auto'}),
344
- dbc.Col([dbc.Label('Date of the hike'), html.Br(),
345
- dcc.DatePickerSingle(id='calendar-date',
346
- placeholder='Select the date of your hike',
347
- display_format='Do MMMM YYYY',
348
- min_date_allowed=date.today(),
349
- max_date_allowed=date.today() + timedelta(days=7),
350
- initial_visible_month=date.today(),
351
- date=date.today()), dcc.Store(id='store-date')], width={'size': 'auto'}),
352
- dbc.Col([dmc.Divider(orientation='vertical', size=2, color='goldenrod', style={'height': 82})], width={'size': 'auto'}),
353
- dbc.Col([html.Div([html.Label('Start time'), html.Br(), html.Br(),
354
- html.Div([dcc.Dropdown(hours, placeholder=hour, value=hour, style=picker_style, id='dropdown-hour'),
355
- dcc.Store(id='store-hour'),
356
- html.Span(':'),
357
- dcc.Dropdown(minutes, placeholder=minute, value=minute, style=picker_style, id='dropdown-minute'),
358
- dcc.Store(id='store-minute')],
359
- style={'border': '1px solid goldenrod',
360
- 'height': '34px',
361
- 'width': '76px',
362
- 'display': 'flex',
363
- 'align-items': 'center',
364
- },
365
- ),
366
- ], style={'font-family': 'Sans'},
367
- ),
368
- ], width={'size': 'auto'}),
369
- dbc.Col([dmc.Divider(orientation='vertical', size=2, color='goldenrod', style={'height': 82})], width={'size': 'auto'}),
370
- dbc.Col([dbc.Label('Average pace (km/h)'), html.Div(dcc.Slider(3, 6.5, 0.5, value=speed, id='slider-pace'), style={'width': '272px'}), dcc.Store(id='store-pace')], width={'size': 'auto'}),
371
- dbc.Col([dmc.Divider(orientation='vertical', size=2, color='goldenrod', style={'height': 82})], width={'size': 'auto'}),
372
- dbc.Col([dbc.Label('Forecast frequency (km)'), html.Div(dcc.Slider(1, 5, 1, value=frequency, id='slider-freq'), style={'width': '170px'}), dcc.Store(id='store-freq')], width={'size': 'auto'}),
373
- dbc.Col([dmc.Divider(orientation='vertical', size=2, color='goldenrod', style={'height': 82})], width={'size': 'auto'}),
374
- dbc.Col([html.Br(), html.Button('Forecast', id='submit-forecast', n_clicks=0,
375
- style={'width': '86px', 'height': '36px', 'background-color': 'goldenrod', 'font-weight': 'bold', 'color': 'white'})],
376
- width={'size': 'auto'}),
377
- ]),
378
- ], style={'font-size': 13, 'font-family': 'sans'}),
379
- html.Div([html.Br(),
380
- dbc.Row([dbc.Col(html.Div('Sunrise '), width={'size': 'auto', 'offset': 9}),
381
- dbc.Col(html.Img(src=sunrise_icon, style={'height':'42px'}), width={'size': 'auto'}),
382
- dbc.Col(html.Div(id='sunrise-time'), width={'size': 'auto'}),
383
- dbc.Col([dmc.Divider(orientation='vertical', size=2, color='goldenrod', style={'height': 22})], width={'size': 'auto'}),
384
- dbc.Col(html.Div('Sunset '), width={'size': 'auto', 'offset': 0}),
385
- dbc.Col(html.Img(src=sunset_icon, style={'height':'42px'}), width={'size': 'auto'}),
386
- dbc.Col(html.Div(id='sunset-time'), width={'size': 'auto'})]),
387
- ], style={'font-size': 13, 'font-family': 'sans'}),
388
- html.Div(id='datatable-div'),
389
- html.Div([dcc.Graph(id='base-figure', clear_on_unhover=True, style={'height': '90vh'})], id='base-figure-div'),
390
  dcc.Tooltip(id='figure-tooltip'),
391
- html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/',
392
- target='_blank', style={'color': 'goldenrod', 'font-size': 15, 'font-family': 'sans', 'text-decoration': 'none'}),
393
- ], style={'text-align': 'center'},),
394
- html.Div([dcc.Link('Powered by Open Meteo', href='https://open-meteo.com/',
395
- target='_blank', style={'color': 'darkslategray', 'font-size': 13, 'font-family': 'sans', 'text-decoration': 'none'}),
396
- ], style={'text-align': 'center'}),
397
  dcc.Interval(
398
- id='interval-component',
399
- interval=6 * 60 * 60 * 1000,
400
- n_intervals=0),
401
  ], id='layout-content')
402
 
403
- layout = dmc.MantineProvider(layout)
404
-
405
  return layout
406
 
407
- app.layout = serve_layout
408
-
409
- # Callbacks
410
-
411
- @callback(Output('store-gpx', 'data'),
412
- Output('name-gpx', 'children'),
413
- Input('upload-gpx', 'contents'),
414
- State('upload-gpx', 'filename'))
415
- def update_gpx(contents, filename):
416
- if filename:
417
- try:
418
- igpx = filename
419
- message = html.Div(['Upload your GPX track ', html.H6(igpx, style={'color': 'darkslategray', 'font-size': 12, 'font-weight': 'bold'})])
420
- content_type, content_string = contents.split(',')
421
- decoded = base64.b64decode(content_string)
422
- gpx_parsed = gpxpy.parse(decoded)
423
- # Convert to a dataframe one point at a time.
424
- points = []
425
- for track in gpx_parsed.tracks:
426
- for segment in track.segments:
427
- for p in segment.points:
428
- points.append({
429
- 'latitude': p.latitude,
430
- 'longitude': p.longitude,
431
- 'altitude': p.elevation,
432
- })
433
- df_gpx = pd.DataFrame.from_records(points)
434
- except Exception:
435
- igpx = 'default_gpx.gpx'
436
- message = html.Div(['Upload your GPX track ', html.H6('The GPX cannot be parsed. Please, upload another file.', style={'color': 'darkslategray', 'font-size': 12, 'font-weight': 'bold'})])
437
- df_gpx = Converter(input_file = igpx).gpx_to_dataframe()
438
- else:
439
- igpx = 'default_gpx.gpx'
440
- message = html.Div(['Upload your GPX track ', html.H6(igpx, style={'color': 'darkslategray', 'font-size': 12, 'font-weight': 'bold'})])
441
- df_gpx = Converter(input_file = igpx).gpx_to_dataframe()
442
-
443
- return df_gpx.to_dict('records'), message
444
-
445
- @callback(Output('store-date', 'data'),
446
- Input('calendar-date', 'date'))
447
- def update_date(value):
448
- if value:
449
- cdate = value
450
- else:
451
- cdate = hdate
452
- return cdate
453
-
454
- @callback(Output('store-hour', 'data'),
455
- Input('dropdown-hour', 'value'))
456
- def update_hour(value):
457
- if value:
458
- hour = value
459
- else:
460
- hour = hour
461
- return hour
462
-
463
- @callback(Output('store-minute', 'data'),
464
- Input('dropdown-minute', 'value'))
465
- def update_minute(value):
466
- if value:
467
- minute = value
468
- else:
469
- minute = minute
470
- return minute
471
-
472
- @callback(Output('store-freq', 'data'),
473
- Input('slider-freq', 'value'))
474
- def update_freq(value):
475
- if value:
476
- frequency = value
477
- else:
478
- frequency = frequency
479
- return frequency
480
-
481
- @callback(Output('store-pace', 'data'),
482
- Input('slider-pace', 'value'))
483
- def update_pace(value):
484
- if value:
485
- speed = value
486
- else:
487
- speed = speed
488
- return speed
489
-
490
- @callback(Output('sunrise-time', 'children'),
491
- Output('sunset-time', 'children'),
492
- Output('datatable-div', 'children'),
493
- Output('base-figure-div', 'children'),
494
- Input('submit-forecast', 'n_clicks'),
495
- State('store-gpx', 'data'),
496
- State('store-date', 'data'),
497
- State('store-hour', 'data'),
498
- State('store-minute', 'data'),
499
- State('store-freq', 'data'),
500
- State('store-pace', 'data'),
501
- prevent_initial_call=False)
502
- def weather_forecast(n_clicks, gpx_json, cdate, h, m, freq, pace):
503
-
504
- global df_wp
505
- global hdate
506
- global hour
507
- global minute
508
- global time
509
- global frequency
510
- global granularity
511
- global speed
512
-
513
- if cdate:
514
- hdate = cdate
515
-
516
- if h:
517
- hour = h
518
-
519
- if m:
520
- minute = m
521
- time = hour + ':' + minute
522
-
523
- if freq:
524
- frequency = freq
525
- granularity = frequency * 1000
526
-
527
- if pace:
528
- speed = pace
529
-
530
- if not gpx_json:
531
- igpx = 'default_gpx.gpx'
532
- df_gpx = Converter(input_file = igpx).gpx_to_dataframe()
533
- gpx_json = df_gpx.to_dict('records')
534
-
535
- if n_clicks >=0:
536
-
537
- gpx_df = pd.DataFrame.from_records(gpx_json)
538
-
539
- df_gpx, df_wp, dfs, sunrise, sunset, centre_lat, centre_lon = parse_gpx(gpx_df, hdate)
540
-
541
- sunrise_div = html.Div([sunrise])
542
- sunset_div = html.Div([sunset])
543
-
544
- table_div = html.Div([dash_table.DataTable(id='datatable-display',
545
- markdown_options = {'html': True},
546
- columns=[{'name': i, 'id': i, 'deletable': False, 'selectable': False, 'presentation': 'markdown'} for i in dfs.columns],
547
- data=dfs.to_dict('records'),
548
- editable=False,
549
- row_deletable=False,
550
- style_as_list_view=True,
551
- style_cell={'fontSize': '12px', 'text-align': 'center', 'margin-bottom':'0'},
552
- css=[dict(selector= 'p', rule= 'margin: 0; text-align: center')],
553
- style_header={'backgroundColor': 'goldenrod', 'color': 'white', 'fontWeight': 'bold'})
554
- ])
555
-
556
- fig = plot_fig(df_gpx, df_wp, centre_lat, centre_lon)
557
-
558
- figure_div = html.Div([dcc.Graph(id='base-figure', figure=fig, clear_on_unhover=True, style={'height': '90vh'})])
559
-
560
- return sunrise_div, sunset_div, table_div, figure_div
561
 
562
  @callback(Output('figure-tooltip', 'show'),
563
  Output('figure-tooltip', 'bbox'),
@@ -572,7 +315,7 @@ def display_hover(hoverData):
572
  bbox = pt['bbox']
573
  num = pt['pointNumber']
574
 
575
- df_row = df_wp.iloc[num].copy()
576
  img_src = df_row['Weather']
577
  txt_src = df_row['dist_read']
578
 
@@ -581,11 +324,15 @@ def display_hover(hoverData):
581
 
582
  return True, bbox, children
583
 
584
- @callback(Output('layout-content', 'children'),
585
  [Input('interval-component', 'n_intervals')])
586
  def refresh_layout(n):
587
- layout = serve_layout()
588
- return layout
 
 
 
589
 
590
  if __name__ == '__main__':
591
  app.run(debug=False, host='0.0.0.0', port=7860)
 
 
1
+ from datetime import datetime, timedelta
 
 
 
2
  import pandas as pd
3
+ import numpy as np
4
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  import srtm
7
  elevation_data = srtm.get_data()
8
 
9
+ import json
10
+ import geopy
11
+ from geopy import distance
12
+ from beaufort_scale.beaufort_scale import beaufort_scale_kmh
13
+
14
+ import requests
15
  import requests_cache
16
  import openmeteo_requests
17
  from retry_requests import retry
18
 
19
+ from dash import Dash, dcc, html, Input, Output, callback, no_update
20
+ from dash_extensions import Purify
21
+ import plotly.graph_objects as go
22
+
23
+ ### UPDATE PEAK LIST ###
24
+
25
+ lat = 49.610755
26
+ lon = 6.13268
27
+ ele = 310
28
+ dist = 100
29
+
30
+ overpass_url = 'https://overpass.private.coffee/api/interpreter'
31
+
32
+ def add_ele(row):
33
+ if str(int(round(row['altitude'], 0))).isnumeric():
34
+ row['altitude'] = row['altitude']
35
+ else:
36
+ row['altitude'] = elevation_data.get_elevation(row['latitude'], row['longitude'], 0)
37
+ return row
38
+
39
+ def eukarney(lat1, lon1):
40
+ p1 = (lat1, lon1)
41
+ p2 = (lat, lon)
42
+ karney = distance.distance(p1, p2).m
43
+ return karney
44
+
45
+ def compute_bbox(lat, lon, dist):
46
+ bearings = [225, 45]
47
+ origin = geopy.Point(lat, lon)
48
+ l = []
49
+
50
+ for bearing in bearings:
51
+ destination = distance.distance(dist).destination(origin, bearing)
52
+ coords = destination.latitude, destination.longitude
53
+ l.extend(coords)
54
+ return l
55
+
56
+ bbox = compute_bbox(lat, lon, dist)
57
+ bbox = ','.join(str(x) for x in compute_bbox(lat, lon, dist))
58
+
59
+ peak_list = 'peak_list.csv'
60
+
61
+ def update_peaks():
62
+
63
+ overpass_query = '[out:json];(nwr[natural=peak](' + bbox + ');nwr[natural=hill](' + bbox + '););out body;'
64
+
65
+ response = requests.get(overpass_url, params={'data': overpass_query})
66
+
67
+ response = response.json()
68
 
69
+ peak_dict = {'name': [], 'latitude': [], 'longitude': [], 'altitude': []}
70
+ for e in response['elements']:
71
+ peak_dict['latitude'].append(float(e['lat']))
72
+ peak_dict['longitude'].append(float(e['lon']))
73
+ if 'name' in e['tags'].keys():
74
+ peak_dict['name'].append(e['tags']['name'])
75
+ else:
76
+ peak_dict['name'].append('Unnamed hill')
77
+ if 'ele' in e['tags'].keys():
78
+ peak_dict['altitude'].append(float(e['tags']['ele']))
79
+ else:
80
+ peak_dict['altitude'].append(elevation_data.get_elevation(e['lat'], e['lon'], 0))
81
 
 
 
 
 
 
82
 
83
+ df = pd.DataFrame.from_dict(peak_dict)
84
+
85
+ df = df.apply(lambda x: add_ele(x), axis=1)
86
+
87
+ df['distances'] = df.apply(lambda x: eukarney(x['latitude'], x['longitude']), axis=1).fillna(0)
88
+
89
+ df['altitude'] = df['altitude'].round(0).astype(int)
90
+
91
+ df.to_csv(peak_list, index=False)
92
+
93
+ return df
94
+
95
+ ### WEATHER FORECAST ###
96
+
97
  cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
98
  retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
99
  openmeteo = openmeteo_requests.Client(session = retry_session)
 
102
  url = 'https://api.open-meteo.com/v1/forecast'
103
  params = {
104
  'timezone': 'auto',
105
+ 'hourly': ['temperature_2m', 'is_day', 'rain', 'weather_code', 'wind_speed_10m', 'snow_depth']
 
106
  }
107
 
108
  # Load the JSON files mapping weather codes to descriptions and icons
 
111
 
112
  # Weather icons URL
113
  icon_url = 'https://raw.githubusercontent.com/basmilius/weather-icons/refs/heads/dev/production/fill/svg/'
 
 
 
 
 
 
 
 
 
 
 
 
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  def map_icons(df):
116
  code = df['weather_code']
117
 
 
145
  rain = 'No rain / No info'
146
  return rain
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  # Obtain the weather forecast for each waypoint at each specific time
149
+ def get_weather(df):
 
 
 
 
150
 
151
+ params['latitude'] = df['latitude']
152
+ params['longitude'] = df['longitude']
153
+ params['elevation'] = df['altitude']
154
 
155
+ now = datetime.now()
 
156
 
157
+ start_period = (now - timedelta(seconds=3600)).strftime('%Y-%m-%dT%H:%M')
158
+ end_period = now.strftime('%Y-%m-%dT%H:%M')
159
 
160
+ params['start_hour'] = start_period
161
+ params['end_hour'] = end_period
 
 
 
 
 
 
162
 
163
  responses = openmeteo.weather_api(url, params=params)
164
 
 
166
  response = responses[0]
167
 
168
  # Process hourly data. The order of variables needs to be the same as requested.
169
+ # currently = response.Current()
170
  hourly = response.Hourly()
171
 
172
+ minutely_temperature_2m = hourly.Variables(0).ValuesAsNumpy()[0]
173
+ is_day = hourly.Variables(1).ValuesAsNumpy()[0]
174
+ rain = hourly.Variables(2).ValuesAsNumpy()[0]
175
+ weather_code = hourly.Variables(3).ValuesAsNumpy()[0]
176
+ minutely_wind_speed_10m = hourly.Variables(4).ValuesAsNumpy()[0]
177
+ snow_depth = hourly.Variables(5).ValuesAsNumpy()[0]
178
 
179
+ df['Temp (°C)'] = minutely_temperature_2m
180
+ df['weather_code'] = weather_code
181
+ df['is_day'] = is_day
182
 
183
  v_rain_intensity = np.vectorize(rain_intensity)
184
+ df['Rain level'] = v_rain_intensity(rain)
185
 
186
  v_beaufort_scale_kmh = np.vectorize(beaufort_scale_kmh)
187
 
188
+ df['Wind level'] = v_beaufort_scale_kmh(minutely_wind_speed_10m, language='en')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ df['Rain (mm/h)'] = rain
191
+ df['Wind (km/h)'] = minutely_wind_speed_10m
192
 
193
+ df['Snow depth (cm)'] = (snow_depth * 100).round(1)
 
194
 
195
+ return df
196
 
 
 
 
197
 
198
+ def format_peaks():
199
 
200
+ if not os.path.isfile(peak_list):
201
+ update_peaks()
202
 
203
+ today = datetime.today()
204
+ modified_date = datetime.fromtimestamp(os.path.getmtime(peak_list))
205
+ peak_age = today - modified_date
206
 
207
+ if peak_age.days > 30:
208
+ update_peaks()
209
 
210
+ df = pd.read_csv(peak_list)
211
+ df = df[df['altitude']>=df['altitude'].quantile(3/4)].copy()
212
+ df = df.sort_values(by='distances',ascending=True).reset_index(drop=True)
213
+ df = df.head(600).copy()
214
 
215
+ df = df.apply(lambda x: get_weather(x), axis=1)
 
 
 
216
 
217
+ df['Temp (°C)'] = df['Temp (°C)'].round(0).astype(int).astype(str) + '°C'
218
+ df['Wind (km/h)'] = df['Wind (km/h)'].round(1).astype(str).replace('0.0', '')
219
+ df['Rain (mm/h)'] = df['Rain (mm/h)'].round(1).astype(str).replace('0.0', '')
220
+ df['distances'] = (df['distances'] / 1000).round(1).astype(str) + ' km'
221
+ df['Snow depth (cm)'] = df['Snow depth (cm)'].astype(str) + ' cm'
222
+ df['altitude'] = df['altitude'].astype(str) + ' m'
223
+ df['is_day'] = df['is_day'].astype(int)
224
 
225
+ df['weather_code'] = df['weather_code'].astype(int)
226
+ df = df.apply(map_icons, axis=1)
227
 
228
+ df['Rain level'] = df['Rain level'].astype(str)
229
+ df['Wind level'] = df['Wind level'].astype(str)
230
 
231
+ df = df.rename(columns={'distances': 'Distance (km)'})
 
 
 
232
 
233
+ df['dist_read'] = ('<p style="font-family:sans; font-size:12px;">' +
234
+ df['name'] + '<br>' +
235
+ df['altitude'] + ' | ' + df['Distance (km)'] + '<br><br>' +
236
+ 'Snow: ' + df['Snow depth (cm)'] + '<br><br>' +
237
+ '<b>' + df['Weather outline'] + '</b><br><br>' +
238
+ df['Temp (°C)'] + '<br><br>' +
239
+ df['Rain level'] + '<br>' +
240
+ df['Wind level'])
241
 
242
+ df = df[(df['Snow depth (cm)'] != '0.0 cm') | (df['Weather outline'].str.lower().str.contains('snow'))].copy()
 
 
 
 
 
 
243
 
244
+ return df
245
 
246
+ def snow_color(row):
247
+ if row['Snow depth (cm)'] == '0.0 cm':
248
+ row['snow_colour'] = 'goldenrod'
249
+ else:
250
+ row['snow_colour'] = 'aqua'
251
+ return row
252
 
253
+ def plot_fig():
 
 
254
 
255
+ global df
256
 
257
+ lat_centre = 49.8464
258
+ lon_centre = 6.0992
259
 
260
+ df = format_peaks()
261
 
262
+ df['snow_colour'] = ''
263
 
264
+ df = df.apply(lambda row: snow_color(row), axis=1)
265
 
266
  fig = go.Figure()
267
 
268
+ fig.add_trace(go.Scattermap(lon=df['longitude'],
269
+ lat=df['latitude'],
270
+ mode='markers', marker=dict(size=24, color=df['snow_colour'], opacity=0.8, symbol='circle'),
271
+ name='circles'))
272
 
273
+ fig.add_trace(go.Scattermap(lon=df['longitude'],
274
+ lat=df['latitude'],
275
+ mode='markers', marker=dict(size=8, opacity=1, symbol='mountain'),
276
+ name='peaks'))
 
 
277
 
278
  fig.update_layout(map_style='open-street-map',
279
+ map=dict(center=dict(lat=lat_centre, lon=lon_centre), zoom=8))
280
 
281
+ fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'circles'}))
282
+ fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'peaks'}))
283
 
284
  return fig
285
 
286
+ app = Dash(__name__)
 
 
 
 
 
287
  server = app.server
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  def serve_layout():
290
 
291
  layout = html.Div([
292
+ html.Div([dcc.Graph(id='base-figure', clear_on_unhover=True, style={'height': '99vh'})], id='base-figure-div'),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  dcc.Tooltip(id='figure-tooltip'),
 
 
 
 
 
 
294
  dcc.Interval(
295
+ id='interval-component',
296
+ interval=60 * 60 * 1000,
297
+ n_intervals=0),
298
  ], id='layout-content')
299
 
 
 
300
  return layout
301
 
302
+ layout_funct = serve_layout
303
+ app.layout = layout_funct
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
  @callback(Output('figure-tooltip', 'show'),
306
  Output('figure-tooltip', 'bbox'),
 
315
  bbox = pt['bbox']
316
  num = pt['pointNumber']
317
 
318
+ df_row = df.iloc[num].copy()
319
  img_src = df_row['Weather']
320
  txt_src = df_row['dist_read']
321
 
 
324
 
325
  return True, bbox, children
326
 
327
+ @callback(Output('base-figure', 'figure'),
328
  [Input('interval-component', 'n_intervals')])
329
  def refresh_layout(n):
330
+ global fig
331
+ global layout_funct
332
+ fig = plot_fig()
333
+ layout_funct = serve_layout
334
+ return fig
335
 
336
  if __name__ == '__main__':
337
  app.run(debug=False, host='0.0.0.0', port=7860)
338
+