mirix commited on
Commit
995bbbc
·
verified ·
1 Parent(s): 5eda809

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +295 -83
  2. requirements.txt +2 -0
app.py CHANGED
@@ -1,18 +1,25 @@
 
1
  import json
2
  import pytz
3
  import numpy as np
4
  import pandas as pd
 
5
  from geopy import distance
6
  import plotly.graph_objects as go
 
 
7
  from gpx_converter import Converter
8
  from sunrisesunset import SunriseSunset
9
- from datetime import datetime, timedelta
10
  from beaufort_scale.beaufort_scale import beaufort_scale_kmh
11
  from timezonefinder import TimezoneFinder
12
  tf = TimezoneFinder()
13
 
14
- from dash import Dash, dcc, html, dash_table, Input, Output, no_update, callback
15
  import dash_bootstrap_components as dbc
 
 
 
16
  from dash_extensions import Purify
17
 
18
  import srtm
@@ -24,12 +31,17 @@ from retry_requests import retry
24
 
25
  ### VARIABLES ###
26
 
 
 
 
 
 
 
27
  # Variables to become widgets
28
  igpx = 'default_gpx.gpx'
29
- date = '2024-12-29'
30
- time = '10:55'
31
- speed = 4.0
32
- granularity = 2000
33
 
34
  # Setup the Open Meteo API client with cache and retry on error
35
  cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
@@ -56,12 +68,12 @@ sunset_icon = icon_url + 'sunset.svg'
56
  ### FUNCTIONS ###
57
 
58
  # Sunrise sunset
59
- def sunrise_sunset(lat_start, lon_start, lat_end, lon_end, date):
60
 
61
  tz = tf.timezone_at(lng=lon_start, lat=lat_start)
62
  zone = pytz.timezone(tz)
63
 
64
- day = datetime.strptime(date, '%Y-%m-%d')
65
 
66
  dt = day.astimezone(zone)
67
 
@@ -132,7 +144,7 @@ def get_weather(df_wp):
132
  params['longitude'] = df_wp['longitude']
133
  params['elevation'] = df_wp['longitude']
134
 
135
- start_dt = datetime.strptime(date + 'T' + time, '%Y-%m-%dT%H:%M')
136
 
137
  delta_dt = start_dt + timedelta(seconds=df_wp['seconds'])
138
  delta_read = delta_dt.strftime('%Y-%m-%dT%H:%M')
@@ -181,21 +193,14 @@ def get_weather(df_wp):
181
  return df_wp
182
 
183
  # Parse the GPX track
184
- def parse_gpx(igpx):
185
-
186
- global centre_lat
187
- global centre_lon
188
- global sunrise
189
- global sunset
190
-
191
- df_gpx = Converter(input_file = igpx).gpx_to_dataframe()
192
 
193
  # Sunrise sunset
194
 
195
  lat_start, lon_start = df_gpx[['latitude', 'longitude']].head(1).values.flatten().tolist()
196
  lat_end, lon_end = df_gpx[['latitude', 'longitude']].tail(1).values.flatten().tolist()
197
 
198
- sunrise, sunset = sunrise_sunset(lat_start, lon_start, lat_end, lon_end, date)
199
 
200
  df_gpx = df_gpx.apply(lambda x: add_ele(x), axis=1)
201
 
@@ -213,7 +218,7 @@ def parse_gpx(igpx):
213
  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)
214
  df_gpx['distance'] = df_gpx['distances'].cumsum().round(decimals = 0).astype(int)
215
 
216
- df_gpx = df_gpx.drop(columns=['lat_shift', 'lon_shift', 'alt_shift', 'distances'])
217
 
218
  start = df_gpx['distance'].min()
219
  finish = df_gpx['distance'].max()
@@ -260,104 +265,134 @@ def parse_gpx(igpx):
260
 
261
  dfs['Weather'] = '<img style="float: right; padding: 0; margin: -6px; display: block;" width=48px; src=' + dfs['Weather'] + '>'
262
 
263
- return [df_gpx, df_wp, dfs]
264
-
265
- df_gpx, df_wp, dfs = parse_gpx(igpx)
266
 
267
  ### PLOTS ###
268
 
269
  # Plot map
270
 
271
- fig = go.Figure()
 
 
272
 
273
- fig.add_trace(go.Scattermap(lon=df_gpx['longitude'],
274
- lat=df_gpx['latitude'],
275
- mode='lines', line=dict(width=4, color='firebrick'),
276
- name='gpx_trace'))
277
 
278
- fig.add_trace(go.Scattermap(lon=df_wp['longitude'],
279
- lat=df_wp['latitude'],
280
- mode='markers+text', marker=dict(size=24, color='firebrick', opacity=0.8, symbol='circle'),
281
- textfont=dict(color='white', weight='bold'),
282
- text=df_wp.index.astype(str),
283
- name='wp_trace'))
284
 
285
- fig.update_layout(map_style='open-street-map',
286
- map=dict(center=dict(lat=centre_lat, lon=centre_lon), zoom=12))
287
 
288
- fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'wp_trace'}))
289
- fig.update_traces(showlegend=False, hoverinfo='skip', hovertemplate=None, selector=({'name': 'gpx_trace'}))
290
 
 
291
 
292
  ### DASH APP ###
293
 
294
- external_stylesheets = [dbc.themes.BOOTSTRAP]
295
 
296
  app = Dash(__name__, external_stylesheets=external_stylesheets)
297
 
298
  server = app.server
299
 
300
- # Callbacks
301
-
302
- @callback(Output('graph-tooltip', 'show'),
303
- Output('graph-tooltip', 'bbox'),
304
- Output('graph-tooltip', 'children'),
305
- Input('graph-basic-2', 'hoverData'))
306
- def display_hover(hoverData):
307
-
308
- if hoverData is None:
309
- return False, no_update, no_update
310
-
311
- pt = hoverData['points'][0]
312
- bbox = pt['bbox']
313
- num = pt['pointNumber']
314
-
315
- df_row = df_wp.iloc[num]
316
- img_src = df_row['Weather']
317
- txt_src = df_row['dist_read']
318
-
319
- children = [html.Div([html.Img(src=img_src, style={'width': '100%'}), Purify(txt_src),],
320
- style={'width': '128px', 'white-space': 'normal'})]
321
 
322
- return True, bbox, children
 
323
 
324
- # Layout
 
 
 
 
 
 
325
 
326
  def serve_layout():
327
 
328
  layout = html.Div([
329
  html.Div([dcc.Link('The Weather for Hikers', href='.',
330
- style={'color': 'darkslategray', 'font-size': 20, 'font-family': 'sans', 'font-weight': 'bold', 'text-decoration': 'none'}),
331
  ]),
332
  html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/',
333
- target='_blank', style={'color': 'goldenrod', 'font-size': 15, 'font-family': 'sans', 'text-decoration': 'none'}),
334
  ]),
335
  html.Div([html.Br(),
336
- dbc.Row([dbc.Col(html.Div('Sunrise '), width={'size': 'auto', 'offset': 4}),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  dbc.Col(html.Img(src=sunrise_icon, style={'height':'42px'}), width={'size': 'auto'}),
338
- dbc.Col(html.Div(sunrise), width={'size': 'auto'}),
339
- dbc.Col(html.Div('Sunset '), width={'size': 'auto', 'offset': 1}),
 
340
  dbc.Col(html.Img(src=sunset_icon, style={'height':'42px'}), width={'size': 'auto'}),
341
- dbc.Col(html.Div(sunset), width={'size': 'auto'})]),
342
  ], style={'font-size': 13, 'font-family': 'sans'}),
343
- html.Div([dash_table.DataTable(
344
- id='datatable-interactivity', markdown_options = {'html': True},
345
- columns=[{'name': i, 'id': i, 'deletable': False, 'selectable': False, 'presentation': 'markdown'} for i in dfs.columns],
346
- data=dfs.to_dict('records'),
347
- editable=False,
348
- row_deletable=False,
349
- style_as_list_view=True,
350
- style_cell={'fontSize': '12px', 'text-align': 'center', 'margin-bottom':'0'},
351
- css=[dict(selector= 'p', rule= 'margin: 0; text-align: center')],
352
- style_header={'backgroundColor': 'goldenrod', 'color': 'white', 'fontWeight': 'bold'}),
353
- dcc.Graph(id='graph-basic-2', figure=fig, clear_on_unhover=True, style={'height': '90vh'}),
354
- dcc.Tooltip(id='graph-tooltip'),
355
- ]),
356
  html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/',
357
  target='_blank', style={'color': 'goldenrod', 'font-size': 15, 'font-family': 'sans', 'text-decoration': 'none'}),
358
  ], style={'text-align': 'center'},),
359
  html.Div([dcc.Link('Powered by Open Meteo', href='https://open-meteo.com/',
360
- target='_blank', style={'color': 'black', 'font-size': 13, 'font-family': 'sans', 'text-decoration': 'none'}),
361
  ], style={'text-align': 'center'}),
362
  dcc.Interval(
363
  id='interval-component',
@@ -365,15 +400,192 @@ def serve_layout():
365
  n_intervals=0),
366
  ], id='layout-content')
367
 
 
 
368
  return layout
369
 
370
  app.layout = serve_layout
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  @callback(Output('layout-content', 'children'),
373
- [Input('interval-component', 'n_intervals')])
374
  def refresh_layout(n):
375
  layout = serve_layout()
376
  return layout
377
 
378
  if __name__ == '__main__':
379
- app.run(debug=True, host='0.0.0.0', port=7860)
 
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
 
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)
 
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
 
 
144
  params['longitude'] = df_wp['longitude']
145
  params['elevation'] = df_wp['longitude']
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')
 
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
 
 
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()
 
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',
 
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'),
564
+ Output('figure-tooltip', 'children'),
565
+ Input('base-figure', 'hoverData'))
566
+ def display_hover(hoverData):
567
+
568
+ if hoverData is None:
569
+ return False, no_update, no_update
570
+
571
+ pt = hoverData['points'][0]
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
+
579
+ children = [html.Div([html.Img(src=img_src, style={'width': '100%'}), Purify(txt_src),],
580
+ style={'width': '128px', 'white-space': 'normal'})]
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)
requirements.txt CHANGED
@@ -14,6 +14,7 @@ dash-core-components==2.0.0
14
  dash-extensions==1.0.19
15
  dash-html-components==2.0.0
16
  dash-table==5.0.0
 
17
  dataclass-wizard==0.30.1
18
  DateTime==5.5
19
  EditorConfig==0.17.0
@@ -57,6 +58,7 @@ tenacity==9.0.0
57
  timezonefinder==6.5.7
58
  typing_extensions==4.12.2
59
  tzdata==2024.2
 
60
  url-normalize==1.4.3
61
  urllib3==2.2.3
62
  Werkzeug==3.0.6
 
14
  dash-extensions==1.0.19
15
  dash-html-components==2.0.0
16
  dash-table==5.0.0
17
+ dash_mantine_components==0.15.1
18
  dataclass-wizard==0.30.1
19
  DateTime==5.5
20
  EditorConfig==0.17.0
 
58
  timezonefinder==6.5.7
59
  typing_extensions==4.12.2
60
  tzdata==2024.2
61
+ tzlocal==5.2
62
  url-normalize==1.4.3
63
  urllib3==2.2.3
64
  Werkzeug==3.0.6