mirix's picture
Upload app.py
16f43b0 verified
import traceback
import os
import json
import pathlib
import gpxpy
import numpy as np
import pandas as pd
import gradio as gr
from datetime import datetime
import pytz
from sunrisesunset import SunriseSunset
from timezonefinder import TimezoneFinder
tf = TimezoneFinder()
from beaufort_scale.beaufort_scale import beaufort_scale_kmh
import srtm
elevation_data = srtm.get_data()
import openmeteo_requests
import requests_cache
from retry_requests import retry
from geopy import distance
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent='FreeLetzWeather')
from apscheduler.schedulers.background import BackgroundScheduler
### Default variables ###
# 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',
'hourly': ['temperature_2m', 'rain', 'wind_speed_10m', 'weather_code', 'is_day']
}
# Weather icons URL
icon_url = 'https://raw.githubusercontent.com/basmilius/weather-icons/refs/heads/dev/production/fill/svg/'
# Custom CSS
css = '''
#button {background: DarkGoldenrod;}
.buttons {color: white;}
#table {height: 1080px;}
.tables {height: 1080px;}
.required-dropdown input:focus {
color: white;
background-color: DarkGoldenrod;
box-shadow: 0 0 0 12px DarkGoldenrod;
}
'''
# Default GPX if none is uploaded
gpx_file = os.path.join(os.getcwd(), 'default_gpx.gpx')
gpx_path = pathlib.Path(gpx_file)
### Functions ###
with open('weather_icons_custom.json', 'r') as file:
icons = json.load(file)
def add_ele(x):
return elevation_data.get_elevation(x['latitude'], x['longitude'], 0)
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'] = '<img style="float: left; padding: 0; margin: -6px; display: block;" width=32px; src=' + icon_url + icon + '>'
df['Weather outline'] = description
return df
# 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 = ''
return rain
def gen_dates_list():
global day_print
global dates_filt
global dates_dict
global dates_list
global day_read
global today
today = datetime.today()
day_read = today.strftime('%A %-d %B')
day_print = '<h2>' + day_read + '</h2>'
dates_aval = pd.date_range(datetime.today(), periods=7).to_pydatetime().tolist()
dates_read = [x.strftime('%A %-d %B %Y') for x in dates_aval]
dates_filt = [x.strftime('%Y-%m-%d') for x in dates_aval]
dates_dict = dict(zip(dates_read, dates_filt))
dates_list = list(dates_dict.keys())
return dates_list
def sunrise_sunset(lat, lon, day):
tz = tf.timezone_at(lng=lon, lat=lat)
zone = pytz.timezone(tz)
dt = day.astimezone(zone)
rs = SunriseSunset(dt, lat=lat, lon=lon, zenith='official')
rise_time, set_time = rs.sun_rise_set
sunrise = rise_time.strftime('%H:%M')
sunset = set_time.strftime('%H:%M')
sunrise_icon = '<img style="float: left;" width="32px" src=' + icon_url + 'sunrise.svg>'
sunset_icon = '<img style="float: left;" width="32px" src=' + icon_url + 'sunset.svg>'
sunrise = '<h6>' + sunrise_icon + ' Sunrise ' + sunrise + '</h6>'
sunset = '<h6>' + sunset_icon + ' Sunset ' + sunset + '</h6>'
return sunrise, sunset
# Download the JSON and filter it per date
def json_parser(date):
global dfs
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.
hourly = response.Hourly()
hourly_temperature_2m = hourly.Variables(0).ValuesAsNumpy()
rain = hourly.Variables(1).ValuesAsNumpy()
hourly_wind_speed_10m = hourly.Variables(2).ValuesAsNumpy()
weather_code = hourly.Variables(3).ValuesAsNumpy()
is_day = hourly.Variables(4).ValuesAsNumpy()
hourly_data = {'date': pd.date_range(
start = pd.to_datetime(hourly.Time(), unit = 's', utc = True),
end = pd.to_datetime(hourly.TimeEnd(), unit = 's', utc = True),
freq = pd.Timedelta(seconds = hourly.Interval()),
inclusive = 'left'
)}
hourly_data['Temp (°C)'] = hourly_temperature_2m.round(0).astype(int)
hourly_data['weather_code'] = weather_code.astype(int)
hourly_data['is_day'] = is_day.astype(int)
v_rain_intensity = np.vectorize(rain_intensity)
hourly_data['Rain level'] = v_rain_intensity(rain)
v_beaufort_scale_kmh = np.vectorize(beaufort_scale_kmh)
hourly_data['Wind level'] = v_beaufort_scale_kmh(hourly_wind_speed_10m, language='en')
hourly_data['Rain (mm/h)'] = rain.round(1)
hourly_data['Wind (km/h)'] = hourly_wind_speed_10m.round(1)
hourly_dataframe = pd.DataFrame(data = hourly_data)
hourly_dataframe['Temp (°C)'] = hourly_dataframe['Temp (°C)'].astype(str) + '°'
hourly_dataframe['Wind (km/h)'] = hourly_dataframe['Wind (km/h)'].astype(str).replace('0.0', '')
hourly_dataframe['Rain (mm/h)'] = hourly_dataframe['Rain (mm/h)'].astype(str).replace('0.0', '')
hourly_dataframe['Time'] = hourly_dataframe['date'].dt.hour.astype(str).str.zfill(2)
hourly_dataframe = hourly_dataframe.apply(map_icons, axis=1)
dfs = hourly_dataframe[hourly_dataframe['date'].dt.strftime('%Y-%m-%d') == date]
dfs = dfs[['Time', 'Weather', 'Weather outline', 'Temp (°C)', 'Rain (mm/h)', 'Rain level', 'Wind (km/h)', 'Wind level']]
dfs = dfs.style.set_properties(**{'border': '0px'})
return dfs
# Extract coordinates and location from GPX file
def coor_gpx(gpx):
def parse_gpx(gpx):
global gpx_name
global params
global lat
global lon
global altitude
global location
global dates_dict
global dates_list
global day_read
global dates
global sunrise
global sunset
with open(gpx) as f:
gpx_parsed = gpxpy.parse(f)
# Convert to a dataframe one point at a time.
points = []
for track in gpx_parsed.tracks:
for segment in track.segments:
for p in segment.points:
points.append({
'latitude': p.latitude,
'longitude': p.longitude,
'elevation': p.elevation,
})
df_gpx = pd.DataFrame.from_records(points)
#gpx_dict = df_gpx.iloc[-1].to_dict()
df_gpx['srtm'] = df_gpx.apply(lambda x: add_ele(x), axis=1)
# Distance estimation function
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)
# 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['srtm'].shift(periods=-1).fillna(df_gpx['srtm'])
# Apply the distance function to the dataframe
df_gpx['distances'] = df_gpx.apply(lambda x: eukarney(x['latitude'], x['longitude'], x['srtm'], 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)
gpx_dict = df_gpx.iloc[(df_gpx.distance - df_gpx.distance.median()).abs().argsort()[:1]].to_dict('records')[0]
params['latitude'] = gpx_dict['latitude']
params['longitude'] = gpx_dict['longitude']
params['elevation'] = gpx_dict['elevation']
lat = params['latitude']
lon = params['longitude']
if params['elevation'] == None:
params['elevation'] = int(round(gpx_dict['srtm'], 0))
else:
params['elevation'] = int(round(params['elevation'], 0))
altitude = params['elevation']
location = geolocator.reverse('{}, {}'.format(lat, lon), zoom=14)
gpx_name = 'You have uploaded <b style="color: #004170;">' + os.path.basename(gpx.name) + '</b>'
location = '<p style="color: #004170">' + str(location) + '</p>'
dates_list = gen_dates_list()
day_read = dates_list[0]
date_filt = datetime.strptime(day_read, '%A %d %B %Y')
date_filt = date_filt.strftime('%Y-%m-%d')
day_print = '<h2>' + day_read + '</h2>'
sunrise, sunset = sunrise_sunset(lat, lon, datetime.strptime(day_read, '%A %d %B %Y'))
dates = gr.Dropdown(choices=dates_list, label='2. Next, pick up the date of your hike', value=dates_list[0], interactive=True, elem_classes='required-dropdown')
dfs = json_parser(date_filt)
try:
parse_gpx(gpx)
except Exception as error:
traceback.print_exc()
parse_gpx(gpx_path)
global gpx_name
gpx_name = '<b style="color: firebrick;">ERROR: Not a valid GPX file. Upload another file.</b>'
return gpx_name, location, dates, day_print, sunrise, sunset, dfs
# Choose a date from the dropdown menu
def date_chooser(day):
global day_read
global sunrise
global sunset
global sunrise_icon
global sunset_icon
global dates_dict
global dates_list
dates_list = gen_dates_list()
day_read = day
day_print = '<h2>' + day_read + '</h2>'
date = datetime.strptime(day, '%A %d %B %Y')
index = dates_list.index(day)
sunrise, sunset = sunrise_sunset(lat, lon, date)
date_filt = date.strftime('%Y-%m-%d')
dfs = json_parser(date_filt)
dates = gr.Dropdown(choices=dates_list, label='2. Next, pick up the date of your hike', value=dates_list[index], interactive=True, elem_classes='required-dropdown')
return day_print, sunrise, sunset, dfs, dates
# Call functions with default values
coor_gpx(gpx_path)
sunrise, sunset = sunrise_sunset(lat, lon, today)
dfs = json_parser(dates_filt[0])
### Gradio app ###
with gr.Blocks(theme='ParityError/Interstellar', css=css, fill_height=True) as demo:
with gr.Column():
with gr.Row():
gr.HTML('<h1 style="color: DarkGoldenrod">Freedom Luxembourg<br><h3 style="color: #004170">The Weather for Hikers</h3></h1>')
with gr.Column():
upload_gpx = gr.UploadButton(label='1. Upload your GPX track', file_count='single', size='lg', file_types=['.gpx', '.GPX'], elem_id='button', elem_classes='buttons', interactive=True)
file_name = gr.HTML('<h6>' + gpx_name + '</h6>')
dates = gr.Dropdown(choices=gen_dates_list(), label='2. Pick up the date of your hike', value=dates_list[0], interactive=True, elem_classes='required-dropdown')
gr.HTML('<h1><br></h1>')
with gr.Row():
choosen_date = gr.HTML(day_print)
loc = gr.HTML('<p style="color: #004170">' + str(location) + '</p>')
sunrise = gr.HTML(sunrise)
sunset = gr.HTML(sunset)
table = gr.DataFrame(dfs, max_height=1000, type='pandas', headers=None, line_breaks=False, interactive=False, wrap=True, visible=True, render=True,
elem_id='table', elem_classes='tables',
datatype=['str', 'html', 'str', 'str', 'str', 'str', 'str', 'str'],
)
gr.HTML('<center>Freedom Luxembourg<br><a style="color: DarkGoldenrod; font-style: italic; text-decoration: none" href="https://www.freeletz.lu/freeletz/" target="_blank">freeletz.lu</a></center>')
gr.HTML('<center>Powered by <a style="color: #004170; text-decoration: none" href="https://open-meteo.com/" target="_blank">Open Meteo</a></center>')
upload_gpx.upload(fn=coor_gpx, inputs=upload_gpx, outputs=[file_name, loc, dates, choosen_date, sunrise, sunset, table])
dates.input(fn=date_chooser, inputs=dates, outputs=[choosen_date, sunrise, sunset, table, dates])
def restart_app():
demo.close()
port = int(os.environ.get('PORT', 7860))
demo.launch(server_name="0.0.0.0", server_port=port)
scheduler = BackgroundScheduler({'apscheduler.timezone': 'Europe/Luxembourg'})
scheduler.add_job(func=restart_app, trigger='cron', hour='05', minute='55')
scheduler.start()
port = int(os.environ.get('PORT', 7860))
demo.launch(server_name="0.0.0.0", server_port=port)