Spaces:
Running
Running
Upload 2 files
Browse files- app.py +295 -83
- 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 |
-
|
30 |
-
time = '
|
31 |
-
|
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,
|
60 |
|
61 |
tz = tf.timezone_at(lng=lon_start, lat=lat_start)
|
62 |
zone = pytz.timezone(tz)
|
63 |
|
64 |
-
day = datetime.strptime(
|
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(
|
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(
|
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,
|
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
|
264 |
-
|
265 |
-
df_gpx, df_wp, dfs = parse_gpx(igpx)
|
266 |
|
267 |
### PLOTS ###
|
268 |
|
269 |
# Plot map
|
270 |
|
271 |
-
|
|
|
|
|
272 |
|
273 |
-
fig.add_trace(go.Scattermap(lon=df_gpx['longitude'],
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
|
278 |
-
fig.add_trace(go.Scattermap(lon=df_wp['longitude'],
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
|
285 |
-
fig.update_layout(map_style='open-street-map',
|
286 |
-
|
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 |
-
#
|
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 |
-
|
|
|
323 |
|
324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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':
|
331 |
]),
|
332 |
html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/',
|
333 |
-
target='_blank', style={'color': 'goldenrod', 'font-size':
|
334 |
]),
|
335 |
html.Div([html.Br(),
|
336 |
-
dbc.Row([
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
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(
|
344 |
-
|
345 |
-
|
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': '
|
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 |
-
|
374 |
def refresh_layout(n):
|
375 |
layout = serve_layout()
|
376 |
return layout
|
377 |
|
378 |
if __name__ == '__main__':
|
379 |
-
app.run(debug=
|
|
|
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
|