1) add uv to weather, 2) add caching to accelerate loading speed

This commit is contained in:
deng
2025-11-27 22:20:54 +08:00
parent 5e8e51ee82
commit 1c0e72f0e1
3 changed files with 206 additions and 108 deletions

View File

@ -3,6 +3,8 @@
# author: deng # author: deng
# date: 20251127 # date: 20251127
from datetime import datetime
import streamlit as st import streamlit as st
import yaml import yaml
from elevation_profile import ElevationProfileRenderer from elevation_profile import ElevationProfileRenderer
@ -30,6 +32,7 @@ class HikingAssistant:
initial_sidebar_state='collapsed', initial_sidebar_state='collapsed',
) )
# [Block1]
# Title and description # Title and description
st.title('⛰️ ' + self._config['app']['page_title']) st.title('⛰️ ' + self._config['app']['page_title'])
@ -41,25 +44,61 @@ class HikingAssistant:
'[Hikingbook](https://zh-tw.hikingbook.net/explore/trails?regions=Taiwan)等網站下載 GPX 檔案', '[Hikingbook](https://zh-tw.hikingbook.net/explore/trails?regions=Taiwan)等網站下載 GPX 檔案',
) )
# [Block2]
# Analysis
if uploaded_file is not None: if uploaded_file is not None:
try: try:
# Process GPX file # Create cache key based on file name and size
with st.spinner('正在解析 GPX 檔案...'): file_key = f'{uploaded_file.name}_{uploaded_file.size}'
gpx_processor = GPXProcessor(uploaded_file)
if not gpx_processor.validate_and_parse(): # Check if file is already processed
st.error('❌ GPX 檔案格式錯誤或不包含有效的軌跡點') if 'gpx_file_key' not in st.session_state or st.session_state.gpx_file_key != file_key:
return # Process GPX file (only when it's a new file)
with st.spinner('正在解析 GPX 檔案...'):
gpx_processor = GPXProcessor(uploaded_file)
# Calculate statistics if not gpx_processor.validate_and_parse():
total_distance = gpx_processor.calculate_distance() st.error('❌ GPX 檔案格式錯誤或不包含有效的軌跡點')
elevation_gain, elevation_loss = gpx_processor.calculate_elevation_gain_loss() return
min_elevation, max_elevation = gpx_processor.get_min_max_elevation()
start_point, end_point = gpx_processor.get_start_end_points() # Calculate statistics
all_points = gpx_processor.get_all_points() total_distance = gpx_processor.calculate_distance()
distances, elevations = gpx_processor.get_elevation_profile_data() elevation_gain, elevation_loss = gpx_processor.calculate_elevation_gain_loss()
gradients = gpx_processor.get_gradients() min_elevation, max_elevation = gpx_processor.get_min_max_elevation()
estimated_time = gpx_processor.calculate_naismith_time() start_point, end_point = gpx_processor.get_start_end_points()
all_points = gpx_processor.get_all_points()
distances, elevations = gpx_processor.get_elevation_profile_data()
gradients = gpx_processor.get_gradients()
estimated_time = gpx_processor.calculate_naismith_time()
# Cache all processed data
st.session_state.gpx_file_key = file_key
st.session_state.total_distance = total_distance
st.session_state.elevation_gain = elevation_gain
st.session_state.elevation_loss = elevation_loss
st.session_state.min_elevation = min_elevation
st.session_state.max_elevation = max_elevation
st.session_state.start_point = start_point
st.session_state.end_point = end_point
st.session_state.all_points = all_points
st.session_state.distances = distances
st.session_state.elevations = elevations
st.session_state.gradients = gradients
st.session_state.estimated_time = estimated_time
# Use cached data
total_distance = st.session_state.total_distance
elevation_gain = st.session_state.elevation_gain
elevation_loss = st.session_state.elevation_loss
min_elevation = st.session_state.min_elevation
max_elevation = st.session_state.max_elevation
start_point = st.session_state.start_point
end_point = st.session_state.end_point
all_points = st.session_state.all_points
distances = st.session_state.distances
elevations = st.session_state.elevations
gradients = st.session_state.gradients
estimated_time = st.session_state.estimated_time
# Display statistics section # Display statistics section
st.header('📊 路線五四三') st.header('📊 路線五四三')
@ -126,70 +165,128 @@ class HikingAssistant:
st.header('☀️ 天氣安抓') st.header('☀️ 天氣安抓')
if start_point: if start_point:
with st.spinner('正在獲取天氣資訊...'): weather_fetcher = WeatherFetcher(forecast_days=self._config['app']['weather']['forecast_days'])
weather_fetcher = WeatherFetcher() weather_key = f'weather_{start_point[0]:.6f}_{start_point[1]:.6f}'
weather_data = weather_fetcher.get_weather(start_point[0], start_point[1])
if weather_data: # Check if weather data is already cached
weather_info = weather_fetcher.format_weather_display(weather_data) if 'weather_cache_key' not in st.session_state or st.session_state.weather_cache_key != weather_key:
# Fetch weather data (only when coordinates change)
with st.spinner('正在獲取天氣資訊...'):
weather_data = weather_fetcher.get_weather(start_point[0], start_point[1])
if weather_info: # Cache weather data
# Current weather st.session_state.weather_cache_key = weather_key
st.subheader('此刻起點天氣') st.session_state.weather_data = weather_data
col1, col2, col3, col4, col5 = st.columns(5) else:
# Use cached weather data
weather_data = st.session_state.weather_data
with col1: if weather_data:
st.metric(label='🌡️ 溫度', value=weather_info['temperature']) daily_times = weather_data.get('daily_time', [])
if daily_times:
# Date selector
date_labels = [datetime.fromisoformat(d).strftime('%Y/%m/%d') for d in daily_times]
selected_date_label = st.selectbox(
'出發日期',
options=date_labels,
index=0,
key='weather_date_selector',
)
selected_index = date_labels.index(selected_date_label)
with col2: st.subheader('起點天氣')
st.metric(label='💧 濕度', value=weather_info['humidity'])
with col3: # Get weather data
# Get precipitation probability from weather_data # Current
precip_prob = weather_data.get('current_precipitation_prob') current_humidity = weather_data.get('current_humidity')
if precip_prob is not None: current_apparent_temperature = weather_data.get('current_apparent_temperature')
st.metric(label='☔ 降雨機率', value=f'{precip_prob}%') current_precipitation_probability = weather_data.get('current_precipitation_probability')
else: current_precipitation = weather_data.get('current_precipitation')
st.metric(label='☔ 降雨機率', value='N/A') current_wind_speed = weather_data.get('current_wind_speed')
current_wind_direction = weather_data.get('current_wind_direction')
# Daily
daily_temp_max = weather_data.get('daily_temp_max', [])
daily_temp_min = weather_data.get('daily_temp_min', [])
daily_apparent_max = weather_data.get('daily_apparent_temp_max', [])
daily_apparent_min = weather_data.get('daily_apparent_temp_min', [])
daily_mean_relative_humidity = weather_data.get('daily_mean_relative_humidity', [])
daily_precip_sum = weather_data.get('daily_precipitation_sum', [])
daily_precip_prob = weather_data.get('daily_precipitation_prob', [])
daily_wind_speed_max = weather_data.get('daily_wind_speed_max', [])
daily_wind_direction_dominant = weather_data.get('daily_wind_direction_dominant', [])
daily_sunrise = weather_data.get('daily_sunrise', [])
daily_sunset = weather_data.get('daily_sunset', [])
daily_uv_index_max = weather_data.get('daily_uv_index_max', [])
with col4: # Row 1: Temperature (3 columns)
st.metric(label='🌧️ 降雨量', value=weather_info['precipitation']) col1, col2, col3 = st.columns(3)
with col1:
st.metric('🔥 最高溫度', f'{daily_temp_max[selected_index]:.1f}°C')
with col2:
st.metric('❄️ 最低溫度', f'{daily_temp_min[selected_index]:.1f}°C')
with col3:
if selected_index == 0:
apparent_temp = current_apparent_temperature
else:
apparent_temp = (daily_apparent_max[selected_index] + daily_apparent_min[selected_index]) / 2
st.metric('🌡️ 體感溫度', f'{apparent_temp:.1f}°C')
with col5: # Row 2: Humidity, Precipitation, Wind (3 columns)
# Get wind direction and convert to flow direction col1, col2, col3 = st.columns(3)
wind_dir_degrees = weather_data.get('current_wind_direction') with col1:
wind_dir_text = weather_fetcher.convert_wind_degrees_to_flow_direction(wind_dir_degrees) if selected_index == 0:
relative_humidity = current_humidity
else:
relative_humidity = daily_mean_relative_humidity[selected_index]
st.metric('💧 相對濕度', f'{relative_humidity}%')
# Get wind speed and level indicator with col2:
wind_speed_value = weather_data.get('current_wind_speed') if selected_index == 0:
wind_level = weather_fetcher.get_wind_speed_indicator(wind_speed_value) precipitation_probability = current_precipitation_probability
precipitation = current_precipitation
else:
precipitation_probability = daily_precip_prob[selected_index]
precipitation = daily_precip_sum[selected_index]
st.metric('🌧️ 降雨機率 / 雨量', f'{precipitation_probability}% / {precipitation:.1f}mm')
st.metric( with col3:
label='💨 風速與流向', wind_dir_text = weather_fetcher.convert_wind_degrees_to_flow_direction(
value=f'{weather_info["wind_speed"]} {wind_dir_text} {wind_level}', degrees=current_wind_direction if selected_index == 0 else daily_wind_direction_dominant[selected_index]
) )
wind_level = weather_fetcher.get_wind_speed_indicator(
wind_speed=current_wind_speed if selected_index == 0 else daily_wind_speed_max[selected_index]
)
st.metric('💨 風速 / 流向', f'{daily_wind_speed_max[selected_index]:.1f} km/h {wind_dir_text} {wind_level}')
# Forecast # Row 3: Sunrise, Sunset
if weather_info['forecast']: col1, col2, col3 = st.columns(3)
st.subheader('未來三天預報') with col1:
forecast_cols = st.columns(3) sunrise_time = datetime.fromisoformat(daily_sunrise[selected_index]).strftime('%H:%M')
st.metric('🌅 日出時間', sunrise_time)
for idx, forecast in enumerate(weather_info['forecast']): with col2:
with forecast_cols[idx]: sunset_time = datetime.fromisoformat(daily_sunset[selected_index]).strftime('%H:%M')
st.markdown(f'**{forecast["date"]}**') st.metric('🌇 日落時間', sunset_time)
st.write(f'🌡️ 高溫: {forecast["temp_max"]}°C')
st.write(f'🌡️ 低溫: {forecast["temp_min"]}°C') with col3:
st.write(f'☔ 降雨機率: {forecast["precip_prob"]}%') uv_index = daily_uv_index_max[selected_index]
else: uv_level = weather_fetcher.get_uv_index_indicator(uv_index)
st.warning(' 無法格式化天氣資料') st.metric(' UV指數', f'{uv_index} {uv_level}')
if selected_index >= 3:
st.info('❗ 三天後的天氣資訊誤差可能較大')
else: else:
st.warning('⚠️ 無法獲取天氣資訊,請稍後再試') st.warning('⚠️ 無法獲取足夠天數的天氣資料')
else:
st.warning('⚠️ 無法獲取天氣資訊,請稍後再試')
else: else:
st.warning('⚠️ 無法確定起點位置') st.warning('⚠️ 無法確定起點位置')
except Exception as e: except Exception as e:
st.error(f'❌ 處理檔案時發生錯誤: {str(e)}') st.error(f'❌ 處理檔案時發生錯誤: {str(e)}')
# [Block3]
# Footer
st.divider() st.divider()
footer_text = f'<div style="text-align:center; font-size: 0.8em"><p>{self._config["app"]["page_footer_text"]}</p></div>' footer_text = f'<div style="text-align:center; font-size: 0.8em"><p>{self._config["app"]["page_footer_text"]}</p></div>'
st.markdown(footer_text, unsafe_allow_html=True) st.markdown(footer_text, unsafe_allow_html=True)

View File

@ -1,4 +1,6 @@
app: app:
page_title: 臺灣登山小幫手 page_title: 臺灣登山小幫手
page_favicon_path: ./assets/favicon_compressed.jpg page_favicon_path: ./assets/favicon_compressed.jpg
page_footer_text: ⚠️ 本服務提供之資訊僅供規劃參考,山區氣候瞬息萬變,請務必依據現場狀況與自身能力進行風險評估<br>Made with ❤️ by <a href="https://gitea.guineapig.love/deng">deng</a> page_footer_text: ⚠️ 本服務提供之資訊僅供規劃參考,山區氣候瞬息萬變,請務必依據現場狀況與自身能力進行風險評估<br>Made with ❤️ by <a href="https://gitea.guineapig.love/deng">deng</a>
weather:
forecast_days: 7

View File

@ -9,10 +9,11 @@ import requests
class WeatherFetcher: class WeatherFetcher:
"""Fetch weather data for hiking locations.""" """Fetch weather data for hiking locations."""
def __init__(self): def __init__(self, api_url='https://api.open-meteo.com/v1/forecast', request_timeout=8, forecast_days=7):
"""Initialize weather fetcher.""" """Initialize weather fetcher."""
self.api_url = 'https://api.open-meteo.com/v1/forecast' self.api_url = api_url
self.request_timeout = 8 # unit: second self.request_timeout = request_timeout
self.forecast_days = max(forecast_days, 1)
def convert_wind_degrees_to_flow_direction(self, degrees): def convert_wind_degrees_to_flow_direction(self, degrees):
"""Convert wind direction from degrees to flow direction emoji. """Convert wind direction from degrees to flow direction emoji.
@ -50,6 +51,29 @@ class WeatherFetcher:
else: else:
return '🔴' # Dangerous return '🔴' # Dangerous
def get_uv_index_indicator(self, uv_index):
"""Get UV index level indicator based on index.
Args:
uv_index: UV index
Returns:
str: Emoji indicator (🟢/🟡/🟠/🔴/🟣)
"""
if uv_index is None:
return ''
if uv_index <= 2:
return '🟢' # Safe
elif uv_index <= 5:
return '🟡' # Caution
elif uv_index <= 7:
return '🟠' # Alert
elif uv_index <= 10:
return '🔴' # Dangerous
else:
return '🟣' # Extreme
def get_weather(self, latitude, longitude): def get_weather(self, latitude, longitude):
"""Fetch weather data for given coordinates. """Fetch weather data for given coordinates.
@ -65,10 +89,14 @@ class WeatherFetcher:
params = { params = {
'latitude': latitude, 'latitude': latitude,
'longitude': longitude, 'longitude': longitude,
'current': 'temperature_2m,relative_humidity_2m,precipitation,wind_speed_10m,wind_direction_10m', 'current': ('relative_humidity_2m,apparent_temperature,precipitation_probability,precipitation,wind_speed_10m,wind_direction_10m',),
'daily': 'temperature_2m_max,temperature_2m_min,precipitation_probability_max', 'daily': (
'temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,'
'relative_humidity_2m_mean,precipitation_sum,precipitation_probability_max,sunrise,sunset,'
'wind_speed_10m_max,wind_direction_10m_dominant,uv_index_max'
),
'timezone': 'auto', 'timezone': 'auto',
'forecast_days': 3, 'forecast_days': self.forecast_days,
} }
response = requests.get(self.api_url, params=params, timeout=self.request_timeout) response = requests.get(self.api_url, params=params, timeout=self.request_timeout)
@ -81,15 +109,24 @@ class WeatherFetcher:
daily = data.get('daily', {}) daily = data.get('daily', {})
weather_info = { weather_info = {
'current_temperature': current.get('temperature_2m'),
'current_humidity': current.get('relative_humidity_2m'), 'current_humidity': current.get('relative_humidity_2m'),
'current_apparent_temperature': current.get('apparent_temperature'),
'current_precipitation_probability': current.get('precipitation_probability'),
'current_precipitation': current.get('precipitation'), 'current_precipitation': current.get('precipitation'),
'current_precipitation_prob': daily.get('precipitation_probability_max', [None])[0], # Today's rain probability
'current_wind_speed': current.get('wind_speed_10m'), 'current_wind_speed': current.get('wind_speed_10m'),
'current_wind_direction': current.get('wind_direction_10m'), 'current_wind_direction': current.get('wind_direction_10m'),
'daily_temp_max': daily.get('temperature_2m_max', []), 'daily_temp_max': daily.get('temperature_2m_max', []),
'daily_temp_min': daily.get('temperature_2m_min', []), 'daily_temp_min': daily.get('temperature_2m_min', []),
'daily_apparent_temp_max': daily.get('apparent_temperature_max', []),
'daily_apparent_temp_min': daily.get('apparent_temperature_min', []),
'daily_mean_relative_humidity': daily.get('relative_humidity_2m_mean', []),
'daily_precipitation_sum': daily.get('precipitation_sum', []),
'daily_precipitation_prob': daily.get('precipitation_probability_max', []), 'daily_precipitation_prob': daily.get('precipitation_probability_max', []),
'daily_wind_speed_max': daily.get('wind_speed_10m_max', []),
'daily_wind_direction_dominant': daily.get('wind_direction_10m_dominant', []),
'daily_sunrise': daily.get('sunrise', []),
'daily_sunset': daily.get('sunset', []),
'daily_uv_index_max': daily.get('uv_index_max', []),
'daily_time': daily.get('time', []), 'daily_time': daily.get('time', []),
} }
@ -101,41 +138,3 @@ class WeatherFetcher:
except Exception as e: except Exception as e:
print(f'天氣資料處理失敗: {str(e)}') print(f'天氣資料處理失敗: {str(e)}')
return None return None
def format_weather_display(self, weather_info):
"""Format weather data for display.
Args:
weather_info: Weather data dictionary from get_weather()
Returns:
dict: Formatted weather data for UI display
"""
if not weather_info:
return None
formatted = {
'temperature': f'{weather_info.get("current_temperature", "N/A")}°C',
'humidity': f'{weather_info.get("current_humidity", "N/A")}%',
'precipitation': f'{weather_info.get("current_precipitation", 0)} mm',
'wind_speed': f'{weather_info.get("current_wind_speed", "N/A")} km/h',
'forecast': [],
}
# Add 3-day forecast
times = weather_info.get('daily_time', [])
temp_max = weather_info.get('daily_temp_max', [])
temp_min = weather_info.get('daily_temp_min', [])
precip_prob = weather_info.get('daily_precipitation_prob', [])
for i in range(min(3, len(times))):
formatted['forecast'].append(
{
'date': times[i],
'temp_max': temp_max[i] if i < len(temp_max) else 'N/A',
'temp_min': temp_min[i] if i < len(temp_min) else 'N/A',
'precip_prob': precip_prob[i] if i < len(precip_prob) else 'N/A',
}
)
return formatted