Spaces:
Sleeping
Sleeping
import json | |
import pytz | |
import numpy as np | |
import pandas as pd | |
from geopy import distance | |
import plotly.graph_objects as go | |
from gpx_converter import Converter | |
from sunrisesunset import SunriseSunset | |
from datetime import datetime, timedelta | |
from beaufort_scale.beaufort_scale import beaufort_scale_kmh | |
from timezonefinder import TimezoneFinder | |
tf = TimezoneFinder() | |
from dash import Dash, dcc, html, dash_table, Input, Output, no_update, callback | |
import dash_bootstrap_components as dbc | |
from dash_extensions import Purify | |
import srtm | |
elevation_data = srtm.get_data() | |
import requests_cache | |
import openmeteo_requests | |
from retry_requests import retry | |
### VARIABLES ### | |
# Variables to become widgets | |
igpx = 'default_gpx.gpx' | |
date = '2024-12-22' | |
time = '10:15' | |
speed = 4.0 | |
granularity = 2000 | |
# Setup the Open Meteo API client with cache and retry on error | |
cache_session = requests_cache.CachedSession('.cache', expire_after = 3600) | |
retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2) | |
openmeteo = openmeteo_requests.Client(session = retry_session) | |
# Open Meteo weather forecast API | |
url = 'https://api.open-meteo.com/v1/forecast' | |
params = { | |
'timezone': 'auto', | |
'minutely_15': ['temperature_2m', 'rain', 'wind_speed_10m', 'weather_code', 'is_day'], | |
'hourly': ['rain'], | |
} | |
# Load the JSON files mapping weather codes to descriptions and icons | |
with open('weather_icons_custom.json', 'r') as file: | |
icons = json.load(file) | |
# Weather icons URL | |
icon_url = 'https://raw.githubusercontent.com/basmilius/weather-icons/refs/heads/dev/production/fill/svg/' | |
sunrise_icon = icon_url + 'sunrise.svg' | |
sunset_icon = icon_url + 'sunset.svg' | |
### FUNCTIONS ### | |
# Sunrise sunset | |
def sunrise_sunset(lat_start, lon_start, lat_end, lon_end, date): | |
tz = tf.timezone_at(lng=lon_start, lat=lat_start) | |
zone = pytz.timezone(tz) | |
day = datetime.strptime(date, '%Y-%m-%d') | |
dt = day.astimezone(zone) | |
rs_start = SunriseSunset(dt, lat=lat_start, lon=lon_start, zenith='official') | |
rise_time = rs_start.sun_rise_set[0] | |
rs_end = SunriseSunset(dt, lat=lat_end, lon=lon_end, zenith='official') | |
set_time = rs_end.sun_rise_set[1] | |
sunrise = rise_time.strftime('%H:%M') | |
sunset = set_time.strftime('%H:%M') | |
return sunrise, sunset | |
# Map weather codes to descriptions and icons | |
def map_icons(df): | |
code = df['weather_code'] | |
if df['is_day'] == 1: | |
icon = icons[str(code)]['day']['icon'] | |
description = icons[str(code)]['day']['description'] | |
elif df['is_day'] == 0: | |
icon = icons[str(code)]['night']['icon'] | |
description = icons[str(code)]['night']['description'] | |
df['Weather'] = icon_url + icon | |
df['Weather outline'] = description | |
return df | |
# Quantitative pluviometry to natural language | |
def rain_intensity(precipt): | |
if precipt >= 50: | |
rain = 'Extreme rain' | |
elif 50 < precipt <= 16: | |
rain = 'Very heavy rain' | |
elif 4 <= precipt < 16: | |
rain = 'Heavy rain' | |
elif 1 <= precipt < 4: | |
rain = 'Moderate rain' | |
elif 0.25 <= precipt < 1: | |
rain = 'Light rain' | |
elif 0 < precipt < 0.25: | |
rain = 'Light drizzle' | |
else: | |
rain = 'No rain / No info' | |
return rain | |
# Function to add elevation | |
def add_ele(row): | |
if pd.isnull(row['altitude']): | |
row['altitude'] = elevation_data.get_elevation(row['latitude'], row['longitude'], 0) | |
else: | |
row['altitude'] = row['altitude'] | |
return row | |
# Compute distances using the Karney algorith with Euclidian altitude correction | |
def eukarney(lat1, lon1, alt1, lat2, lon2, alt2): | |
p1 = (lat1, lon1) | |
p2 = (lat2, lon2) | |
karney = distance.distance(p1, p2).m | |
return np.sqrt(karney**2 + (alt2 - alt1)**2) | |
# Obtain the weather forecast for each waypoint at each specific time | |
def get_weather(df_wp): | |
params['latitude'] = df_wp['latitude'] | |
params['longitude'] = df_wp['longitude'] | |
params['elevation'] = df_wp['longitude'] | |
start_dt = datetime.strptime(date + 'T' + time, '%Y-%m-%dT%H:%M') | |
delta_dt = start_dt + timedelta(seconds=df_wp['seconds']) | |
delta_read = delta_dt.strftime('%Y-%m-%dT%H:%M') | |
start_period = (delta_dt - timedelta(seconds=1800)).strftime('%Y-%m-%dT%H:%M') | |
end_period = (delta_dt + timedelta(seconds=1800)).strftime('%Y-%m-%dT%H:%M') | |
time_read = delta_dt.strftime('%H:%M') | |
df_wp['Time'] = time_read | |
params['start_minutely_15'] = delta_read | |
params['end_minutely_15'] = delta_read | |
params['start_hour'] = delta_read | |
params['end_hour'] = delta_read | |
responses = openmeteo.weather_api(url, params=params) | |
# Process first location. Add a for-loop for multiple locations or weather models | |
response = responses[0] | |
# Process hourly data. The order of variables needs to be the same as requested. | |
minutely = response.Minutely15() | |
hourly = response.Hourly() | |
minutely_temperature_2m = minutely.Variables(0).ValuesAsNumpy()[0] | |
rain = hourly.Variables(0).ValuesAsNumpy()[0] | |
minutely_wind_speed_10m = minutely.Variables(2).ValuesAsNumpy()[0] | |
weather_code = minutely.Variables(3).ValuesAsNumpy()[0] | |
is_day = minutely.Variables(4).ValuesAsNumpy()[0] | |
df_wp['Temp (°C)'] = minutely_temperature_2m | |
df_wp['weather_code'] = weather_code | |
df_wp['is_day'] = is_day | |
v_rain_intensity = np.vectorize(rain_intensity) | |
df_wp['Rain level'] = v_rain_intensity(rain) | |
v_beaufort_scale_kmh = np.vectorize(beaufort_scale_kmh) | |
df_wp['Wind level'] = v_beaufort_scale_kmh(minutely_wind_speed_10m, language='en') | |
df_wp['Rain (mm/h)'] = rain.round(1) | |
df_wp['Wind (km/h)'] = minutely_wind_speed_10m.round(1) | |
return df_wp | |
# Parse the GPX track | |
def parse_gpx(igpx): | |
global centre_lat | |
global centre_lon | |
global sunrise | |
global sunset | |
df_gpx = Converter(input_file = igpx).gpx_to_dataframe() | |
# Sunrise sunset | |
lat_start, lon_start = df_gpx[['latitude', 'longitude']].head(1).values.flatten().tolist() | |
lat_end, lon_end = df_gpx[['latitude', 'longitude']].tail(1).values.flatten().tolist() | |
sunrise, sunset = sunrise_sunset(lat_start, lon_start, lat_end, lon_end, date) | |
df_gpx = df_gpx.apply(lambda x: add_ele(x), axis=1) | |
centre_lat = (df_gpx['latitude'].max() + df_gpx['latitude'].min()) / 2 | |
centre_lon = (df_gpx['longitude'].max() + df_gpx['longitude'].min()) / 2 | |
# Create shifted columns in order to facilitate distance calculation | |
df_gpx['lat_shift'] = df_gpx['latitude'].shift(periods=-1).fillna(df_gpx['latitude']) | |
df_gpx['lon_shift'] = df_gpx['longitude'].shift(periods=-1).fillna(df_gpx['longitude']) | |
df_gpx['alt_shift'] = df_gpx['altitude'].shift(periods=-1).fillna(df_gpx['altitude']) | |
# Apply the distance function to the dataframe | |
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) | |
df_gpx['distance'] = df_gpx['distances'].cumsum().round(decimals = 0).astype(int) | |
df_gpx = df_gpx.drop(columns=['lat_shift', 'lon_shift', 'alt_shift', 'distances']) | |
start = df_gpx['distance'].min() | |
finish = df_gpx['distance'].max() | |
dist_rang = list(range(start, finish, granularity)) | |
dist_rang.append(finish) | |
way_list = [] | |
for waypoint in dist_rang: | |
gpx_dict = df_gpx.iloc[(df_gpx.distance - waypoint).abs().argsort()[:1]].to_dict('records')[0] | |
way_list.append(gpx_dict) | |
df_wp = pd.DataFrame(way_list) | |
df_wp['seconds'] = df_wp['distance'].apply(lambda x: int(round(x / (speed * (5/18)), 0))) | |
df_wp = df_wp.apply(lambda x: get_weather(x), axis=1) | |
df_wp['Temp (°C)'] = df_wp['Temp (°C)'].round(0).astype(int).astype(str) + '°C' | |
df_wp['is_day'] = df_wp['is_day'].astype(int) | |
df_wp['weather_code'] = df_wp['weather_code'].astype(int) | |
df_wp = df_wp.apply(map_icons, axis=1) | |
df_wp['Rain level'] = df_wp['Rain level'].astype(str) | |
df_wp['Wind level'] = df_wp['Wind level'].astype(str) | |
df_wp['dist_read'] = ('<p style="font-family:sans; font-size:14px;"><b>' + | |
df_wp['Weather outline'] + '</b><br><br>' + | |
df_wp['Temp (°C)'] + '<br><br>' + | |
df_wp['Rain level'] + '<br>' + | |
df_wp['Wind level'] + '<br><br>' + | |
df_wp['Time'] + '<br><br>' + | |
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>') | |
df_wp = df_wp.reset_index(drop=True) | |
df_wp['Waypoint'] = df_wp.index | |
dfs = df_wp[['Waypoint', 'Time', 'Weather', 'Weather outline', 'Temp (°C)', 'Rain (mm/h)', 'Rain level', 'Wind (km/h)', 'Wind level']].copy() | |
dfs['Wind (km/h)'] = dfs['Wind (km/h)'].round(1).astype(str).replace('0.0', '') | |
dfs['Rain (mm/h)'] = dfs['Rain (mm/h)'].round(1).astype(str).replace('0.0', '') | |
dfs['Temp (°C)'] = dfs['Temp (°C)'].str.replace('C', '') | |
dfs['Weather'] = '<img style="float: right; padding: 0; margin: -6px; display: block;" width=48px; src=' + dfs['Weather'] + '>' | |
return [df_gpx, df_wp, dfs] | |
df_gpx, df_wp, dfs = parse_gpx(igpx) | |
### PLOTS ### | |
# Plot map | |
fig = go.Figure() | |
fig.add_trace(go.Scattermap(lon=df_gpx['longitude'], | |
lat=df_gpx['latitude'], | |
mode='lines', line=dict(width=4, color='firebrick'), | |
name='gpx_trace')) | |
fig.add_trace(go.Scattermap(lon=df_wp['longitude'], | |
lat=df_wp['latitude'], | |
mode='markers+text', marker=dict(size=24, color='firebrick', opacity=0.8, symbol='circle'), | |
textfont=dict(color='white', weight='bold'), | |
text=df_wp.index.astype(str), | |
name='wp_trace')) | |
fig.update_layout(map_style='open-street-map', | |
map=dict(center=dict(lat=centre_lat, lon=centre_lon), zoom=12)) | |
fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'wp_trace'})) | |
fig.update_traces(showlegend=False, hoverinfo='skip', hovertemplate=None, selector=({'name': 'gpx_trace'})) | |
### DASH APP ### | |
external_stylesheets = [dbc.themes.BOOTSTRAP] | |
app = Dash(__name__, external_stylesheets=external_stylesheets) | |
# Callbacks | |
def display_hover(hoverData): | |
if hoverData is None: | |
return False, no_update, no_update | |
pt = hoverData['points'][0] | |
bbox = pt['bbox'] | |
num = pt['pointNumber'] | |
df_row = df_wp.iloc[num] | |
img_src = df_row['Weather'] | |
txt_src = df_row['dist_read'] | |
children = [html.Div([html.Img(src=img_src, style={'width': '100%'}), Purify(txt_src),], | |
style={'width': '128px', 'white-space': 'normal'})] | |
return True, bbox, children | |
# Layout | |
app.layout = html.Div([ | |
html.Div([dcc.Link('The Weather for Hikers', href='.', | |
style={'color': 'darkslategray', 'font-size': 20, 'font-family': 'sans', 'font-weight': 'bold', 'text-decoration': 'none'}), | |
]), | |
html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/', | |
target='_blank', style={'color': 'goldenrod', 'font-size': 15, 'font-family': 'sans', 'text-decoration': 'none'}), | |
]), | |
html.Div([html.Br(), | |
dbc.Row([dbc.Col(html.Div('Sunrise '), width={'size': 'auto', 'offset': 4}), | |
dbc.Col(html.Img(src=sunrise_icon, style={'height':'42px'}), width={'size': 'auto'}), | |
dbc.Col(html.Div(sunrise), width={'size': 'auto'}), | |
dbc.Col(html.Div('Sunset '), width={'size': 'auto', 'offset': 1}), | |
dbc.Col(html.Img(src=sunset_icon, style={'height':'42px'}), width={'size': 'auto'}), | |
dbc.Col(html.Div(sunset), width={'size': 'auto'})]), | |
], style={'font-size': 13, 'font-family': 'sans'}), | |
html.Div([dash_table.DataTable( | |
id='datatable-interactivity', markdown_options = {'html': True}, | |
columns=[{'name': i, 'id': i, 'deletable': False, 'selectable': False, 'presentation': 'markdown'} for i in dfs.columns], | |
data=dfs.to_dict('records'), | |
editable=False, | |
row_deletable=False, | |
style_as_list_view=True, | |
style_cell={'fontSize': '12px', 'text-align': 'center', 'margin-bottom':'0'}, | |
css=[dict(selector= 'p', rule= 'margin: 0; text-align: center')], | |
style_header={'backgroundColor': 'goldenrod', 'color': 'white', 'fontWeight': 'bold'}), | |
dcc.Graph(id='graph-basic-2', figure=fig, clear_on_unhover=True, style={'height': '90vh'}), | |
dcc.Tooltip(id='graph-tooltip'), | |
]), | |
html.Div([dcc.Link('Freedom Luxembourg', href='https://www.freeletz.lu/freeletz/', | |
target='_blank', style={'color': 'goldenrod', 'font-size': 15, 'font-family': 'sans', 'text-decoration': 'none'}), | |
], style={'text-align': 'center'},), | |
html.Div([dcc.Link('Powered by Open Meteo', href='https://open-meteo.com/', | |
target='_blank', style={'color': 'black', 'font-size': 13, 'font-family': 'sans', 'text-decoration': 'none'}), | |
], style={'text-align': 'center'},) | |
]) | |
# Light up server | |
if __name__ == '__main__': | |
app.run(debug=False, host='0.0.0.0', port=7860) | |