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'] = ('
' +
df_wp['Weather outline'] + '
' +
df_wp['Temp (°C)'] + '
' +
df_wp['Rain level'] + '
' +
df_wp['Wind level'] + '
' +
df_wp['Time'] + '
' +
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