diff --git a/hiking_assistant/app.py b/hiking_assistant/app.py index d6f8ab1..b9268bd 100644 --- a/hiking_assistant/app.py +++ b/hiking_assistant/app.py @@ -3,6 +3,8 @@ # author: deng # date: 20251127 +from datetime import datetime + import streamlit as st import yaml from elevation_profile import ElevationProfileRenderer @@ -30,6 +32,7 @@ class HikingAssistant: initial_sidebar_state='collapsed', ) + # [Block1] # Title and description st.title('⛰️ ' + self._config['app']['page_title']) @@ -41,25 +44,61 @@ class HikingAssistant: '[Hikingbook](https://zh-tw.hikingbook.net/explore/trails?regions=Taiwan)等網站下載 GPX 檔案', ) + # [Block2] + # Analysis if uploaded_file is not None: try: - # Process GPX file - with st.spinner('正在解析 GPX 檔案...'): - gpx_processor = GPXProcessor(uploaded_file) + # Create cache key based on file name and size + file_key = f'{uploaded_file.name}_{uploaded_file.size}' - if not gpx_processor.validate_and_parse(): - st.error('❌ GPX 檔案格式錯誤或不包含有效的軌跡點') - return + # Check if file is already processed + if 'gpx_file_key' not in st.session_state or st.session_state.gpx_file_key != file_key: + # Process GPX file (only when it's a new file) + with st.spinner('正在解析 GPX 檔案...'): + gpx_processor = GPXProcessor(uploaded_file) - # Calculate statistics - total_distance = gpx_processor.calculate_distance() - elevation_gain, elevation_loss = gpx_processor.calculate_elevation_gain_loss() - min_elevation, max_elevation = gpx_processor.get_min_max_elevation() - 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() + if not gpx_processor.validate_and_parse(): + st.error('❌ GPX 檔案格式錯誤或不包含有效的軌跡點') + return + + # Calculate statistics + total_distance = gpx_processor.calculate_distance() + elevation_gain, elevation_loss = gpx_processor.calculate_elevation_gain_loss() + min_elevation, max_elevation = gpx_processor.get_min_max_elevation() + 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 st.header('📊 路線五四三') @@ -126,70 +165,128 @@ class HikingAssistant: st.header('☀️ 天氣安抓') if start_point: - with st.spinner('正在獲取天氣資訊...'): - weather_fetcher = WeatherFetcher() - weather_data = weather_fetcher.get_weather(start_point[0], start_point[1]) + weather_fetcher = WeatherFetcher(forecast_days=self._config['app']['weather']['forecast_days']) + weather_key = f'weather_{start_point[0]:.6f}_{start_point[1]:.6f}' - if weather_data: - weather_info = weather_fetcher.format_weather_display(weather_data) + # Check if weather data is already cached + 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: - # Current weather - st.subheader('此刻起點天氣') - col1, col2, col3, col4, col5 = st.columns(5) + # Cache weather data + st.session_state.weather_cache_key = weather_key + st.session_state.weather_data = weather_data + else: + # Use cached weather data + weather_data = st.session_state.weather_data - with col1: - st.metric(label='🌡️ 溫度', value=weather_info['temperature']) + if weather_data: + 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.metric(label='💧 濕度', value=weather_info['humidity']) + st.subheader('起點天氣') - with col3: - # Get precipitation probability from weather_data - precip_prob = weather_data.get('current_precipitation_prob') - if precip_prob is not None: - st.metric(label='☔ 降雨機率', value=f'{precip_prob}%') - else: - st.metric(label='☔ 降雨機率', value='N/A') + # Get weather data + # Current + current_humidity = weather_data.get('current_humidity') + current_apparent_temperature = weather_data.get('current_apparent_temperature') + current_precipitation_probability = weather_data.get('current_precipitation_probability') + current_precipitation = weather_data.get('current_precipitation') + 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: - st.metric(label='🌧️ 降雨量', value=weather_info['precipitation']) + # Row 1: Temperature (3 columns) + 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: - # Get wind direction and convert to flow direction - wind_dir_degrees = weather_data.get('current_wind_direction') - wind_dir_text = weather_fetcher.convert_wind_degrees_to_flow_direction(wind_dir_degrees) + # Row 2: Humidity, Precipitation, Wind (3 columns) + col1, col2, col3 = st.columns(3) + with col1: + 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 - wind_speed_value = weather_data.get('current_wind_speed') - wind_level = weather_fetcher.get_wind_speed_indicator(wind_speed_value) + with col2: + if selected_index == 0: + 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( - label='💨 風速與流向', - value=f'{weather_info["wind_speed"]} {wind_dir_text} {wind_level}', - ) + with col3: + wind_dir_text = weather_fetcher.convert_wind_degrees_to_flow_direction( + 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 - if weather_info['forecast']: - st.subheader('未來三天預報') - forecast_cols = st.columns(3) + # Row 3: Sunrise, Sunset + col1, col2, col3 = st.columns(3) + with col1: + sunrise_time = datetime.fromisoformat(daily_sunrise[selected_index]).strftime('%H:%M') + st.metric('🌅 日出時間', sunrise_time) - for idx, forecast in enumerate(weather_info['forecast']): - with forecast_cols[idx]: - st.markdown(f'**{forecast["date"]}**') - st.write(f'🌡️ 高溫: {forecast["temp_max"]}°C') - st.write(f'🌡️ 低溫: {forecast["temp_min"]}°C') - st.write(f'☔ 降雨機率: {forecast["precip_prob"]}%') - else: - st.warning('⚠️ 無法格式化天氣資料') + with col2: + sunset_time = datetime.fromisoformat(daily_sunset[selected_index]).strftime('%H:%M') + st.metric('🌇 日落時間', sunset_time) + + with col3: + uv_index = daily_uv_index_max[selected_index] + uv_level = weather_fetcher.get_uv_index_indicator(uv_index) + st.metric('☀️ UV指數', f'{uv_index} {uv_level}') + + if selected_index >= 3: + st.info('❗ 三天後的天氣資訊誤差可能較大') else: - st.warning('⚠️ 無法獲取天氣資訊,請稍後再試') + st.warning('⚠️ 無法獲取足夠天數的天氣資料') + else: + st.warning('⚠️ 無法獲取天氣資訊,請稍後再試') else: st.warning('⚠️ 無法確定起點位置') except Exception as e: st.error(f'❌ 處理檔案時發生錯誤: {str(e)}') + # [Block3] + # Footer st.divider() footer_text = f'
{self._config["app"]["page_footer_text"]}