Spaces:
Running
Running
from datetime import datetime, timedelta | |
import pandas as pd | |
import numpy as np | |
import os | |
import srtm | |
elevation_data = srtm.get_data() | |
import json | |
import geopy | |
from geopy import distance | |
from beaufort_scale.beaufort_scale import beaufort_scale_kmh | |
import requests | |
import requests_cache | |
import openmeteo_requests | |
from retry_requests import retry | |
from dash import Dash, dcc, html, Input, Output, callback, no_update | |
from dash_extensions import Purify | |
import plotly.graph_objects as go | |
### UPDATE PEAK LIST ### | |
lat = 49.610755 | |
lon = 6.13268 | |
ele = 310 | |
dist = 100 | |
overpass_url = 'https://overpass.private.coffee/api/interpreter' | |
def add_ele(row): | |
if str(int(round(row['altitude'], 0))).isnumeric(): | |
row['altitude'] = row['altitude'] | |
else: | |
row['altitude'] = elevation_data.get_elevation(row['latitude'], row['longitude'], 0) | |
return row | |
def eukarney(lat1, lon1): | |
p1 = (lat1, lon1) | |
p2 = (lat, lon) | |
karney = distance.distance(p1, p2).m | |
return karney | |
def compute_bbox(lat, lon, dist): | |
bearings = [225, 45] | |
origin = geopy.Point(lat, lon) | |
l = [] | |
for bearing in bearings: | |
destination = distance.distance(dist).destination(origin, bearing) | |
coords = destination.latitude, destination.longitude | |
l.extend(coords) | |
return l | |
bbox = compute_bbox(lat, lon, dist) | |
bbox = ','.join(str(x) for x in compute_bbox(lat, lon, dist)) | |
peak_list = 'peak_list.csv' | |
def update_peaks(): | |
overpass_query = '[out:json];(nwr[natural=peak](' + bbox + ');nwr[natural=hill](' + bbox + '););out body;' | |
response = requests.get(overpass_url, params={'data': overpass_query}) | |
response = response.json() | |
peak_dict = {'name': [], 'latitude': [], 'longitude': [], 'altitude': []} | |
for e in response['elements']: | |
peak_dict['latitude'].append(float(e['lat'])) | |
peak_dict['longitude'].append(float(e['lon'])) | |
if 'name' in e['tags'].keys(): | |
peak_dict['name'].append(e['tags']['name']) | |
else: | |
peak_dict['name'].append('Unnamed hill') | |
if 'ele' in e['tags'].keys(): | |
peak_dict['altitude'].append(float(e['tags']['ele'])) | |
else: | |
peak_dict['altitude'].append(elevation_data.get_elevation(e['lat'], e['lon'], 0)) | |
df = pd.DataFrame.from_dict(peak_dict) | |
df = df.apply(lambda x: add_ele(x), axis=1) | |
df['distances'] = df.apply(lambda x: eukarney(x['latitude'], x['longitude']), axis=1).fillna(0) | |
df['altitude'] = df['altitude'].round(0).astype(int) | |
df.to_csv(peak_list, index=False) | |
return df | |
### WEATHER FORECAST ### | |
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', 'is_day', 'rain', 'weather_code', 'wind_speed_10m', 'snow_depth'] | |
} | |
# 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/' | |
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 | |
# Obtain the weather forecast for each waypoint at each specific time | |
def get_weather(df): | |
params['latitude'] = df['latitude'] | |
params['longitude'] = df['longitude'] | |
params['elevation'] = df['altitude'] | |
now = datetime.now() | |
start_period = (now - timedelta(seconds=3600)).strftime('%Y-%m-%dT%H:%M') | |
end_period = now.strftime('%Y-%m-%dT%H:%M') | |
params['start_hour'] = start_period | |
params['end_hour'] = end_period | |
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. | |
# currently = response.Current() | |
hourly = response.Hourly() | |
minutely_temperature_2m = hourly.Variables(0).ValuesAsNumpy()[0] | |
is_day = hourly.Variables(1).ValuesAsNumpy()[0] | |
rain = hourly.Variables(2).ValuesAsNumpy()[0] | |
weather_code = hourly.Variables(3).ValuesAsNumpy()[0] | |
minutely_wind_speed_10m = hourly.Variables(4).ValuesAsNumpy()[0] | |
snow_depth = hourly.Variables(5).ValuesAsNumpy()[0] | |
df['Temp (°C)'] = minutely_temperature_2m | |
df['weather_code'] = weather_code | |
df['is_day'] = is_day | |
v_rain_intensity = np.vectorize(rain_intensity) | |
df['Rain level'] = v_rain_intensity(rain) | |
v_beaufort_scale_kmh = np.vectorize(beaufort_scale_kmh) | |
df['Wind level'] = v_beaufort_scale_kmh(minutely_wind_speed_10m, language='en') | |
df['Rain (mm/h)'] = rain | |
df['Wind (km/h)'] = minutely_wind_speed_10m | |
df['Snow depth (cm)'] = (snow_depth * 100).round(1) | |
return df | |
def format_peaks(): | |
if not os.path.isfile(peak_list): | |
update_peaks() | |
today = datetime.today() | |
modified_date = datetime.fromtimestamp(os.path.getmtime(peak_list)) | |
peak_age = today - modified_date | |
if peak_age.days > 30: | |
update_peaks() | |
df = pd.read_csv(peak_list) | |
df = df[df['altitude']>=df['altitude'].quantile(3/4)].copy() | |
df = df.sort_values(by='distances',ascending=True).reset_index(drop=True) | |
df = df.head(600).copy() | |
df = df.apply(lambda x: get_weather(x), axis=1) | |
df['Temp (°C)'] = df['Temp (°C)'].round(0).astype(int).astype(str) + '°C' | |
df['Wind (km/h)'] = df['Wind (km/h)'].round(1).astype(str).replace('0.0', '') | |
df['Rain (mm/h)'] = df['Rain (mm/h)'].round(1).astype(str).replace('0.0', '') | |
df['distances'] = (df['distances'] / 1000).round(1).astype(str) + ' km' | |
df['Snow depth (cm)'] = df['Snow depth (cm)'].astype(str) + ' cm' | |
df['altitude'] = df['altitude'].astype(str) + ' m' | |
df['is_day'] = df['is_day'].astype(int) | |
df['weather_code'] = df['weather_code'].astype(int) | |
df = df.apply(map_icons, axis=1) | |
df['Rain level'] = df['Rain level'].astype(str) | |
df['Wind level'] = df['Wind level'].astype(str) | |
df = df.rename(columns={'distances': 'Distance (km)'}) | |
df['dist_read'] = ('<p style="font-family:sans; font-size:12px;">' + | |
df['name'] + '<br>' + | |
df['altitude'] + ' | ' + df['Distance (km)'] + '<br><br>' + | |
'Snow: ' + df['Snow depth (cm)'] + '<br><br>' + | |
'<b>' + df['Weather outline'] + '</b><br><br>' + | |
df['Temp (°C)'] + '<br><br>' + | |
df['Rain level'] + '<br>' + | |
df['Wind level']) | |
df = df[(df['Snow depth (cm)'] != '0.0 cm') | (df['Weather outline'].str.lower().str.contains('snow'))].copy() | |
return df | |
def snow_color(row): | |
if row['Snow depth (cm)'] == '0.0 cm': | |
row['snow_colour'] = 'goldenrod' | |
else: | |
row['snow_colour'] = 'aqua' | |
return row | |
def plot_fig(): | |
global df | |
lat_centre = 49.8464 | |
lon_centre = 6.0992 | |
df = format_peaks() | |
df['snow_colour'] = '' | |
df = df.apply(lambda row: snow_color(row), axis=1) | |
fig = go.Figure() | |
fig.add_trace(go.Scattermap(lon=df['longitude'], | |
lat=df['latitude'], | |
mode='markers', marker=dict(size=24, color=df['snow_colour'], opacity=0.8, symbol='circle'), | |
name='circles')) | |
fig.add_trace(go.Scattermap(lon=df['longitude'], | |
lat=df['latitude'], | |
mode='markers', marker=dict(size=8, opacity=1, symbol='mountain'), | |
name='peaks')) | |
fig.update_layout(map_style='open-street-map', | |
map=dict(center=dict(lat=lat_centre, lon=lon_centre), zoom=8)) | |
fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'circles'})) | |
fig.update_traces(showlegend=False, hoverinfo='none', hovertemplate=None, selector=({'name': 'peaks'})) | |
return fig | |
app = Dash(__name__) | |
server = app.server | |
fig = plot_fig() | |
def serve_layout(): | |
layout = html.Div([ | |
html.Div([dcc.Graph(id='base-figure', figure=fig, clear_on_unhover=True, style={'height': '99vh'})], id='base-figure-div'), | |
dcc.Tooltip(id='figure-tooltip'), | |
dcc.Interval( | |
id='interval-component', | |
interval=60 * 60 * 1000, | |
n_intervals=0), | |
], id='layout-content') | |
return layout | |
app.layout = serve_layout | |
def refresh_layout(n): | |
global fig | |
fig = plot_fig() | |
layout = serve_layout() | |
return fig, layout | |
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.iloc[num].copy() | |
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': '96px', 'white-space': 'normal'})] | |
return True, bbox, children | |
if __name__ == '__main__': | |
app.run(debug=False, host='0.0.0.0', port=7860) | |