1) refactor, 2) add dockerfile

This commit is contained in:
deng
2025-11-27 19:33:26 +08:00
parent ad753359b0
commit 5e8e51ee82
13 changed files with 406 additions and 290 deletions

2
.gitignore vendored
View File

@ -8,3 +8,5 @@ wheels/
# Virtual environments
.venv
*.tar

51
Dockerfile Normal file
View File

@ -0,0 +1,51 @@
# Modified by official dockerfile sample(https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile)
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
# Setup a non-root user
RUN groupadd --system --gid 999 nonroot \
&& useradd --system --gid 999 --uid 999 --create-home nonroot
# Install the project into `/app`
WORKDIR /app
# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1
# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy
# Ensure installed tools can be executed out of the box
ENV UV_TOOL_BIN_DIR=/usr/local/bin
# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev
# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY ./hiking_assistant /app
COPY pyproject.toml /app
COPY uv.lock /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"
# Reset the entrypoint, don't invoke `uv`
ENTRYPOINT []
# Use the non-root user to run our application
USER nonroot
# Run the Streamlit application by default
ENV TZ="Asia/Taipei"
EXPOSE 8504
CMD ["python", "-m", "streamlit", "run", "app.py", "--server.port=8504", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
# Build
# docker build --platform=linux/amd64 -t hiking_assistant:latest .
# docker save -o hiking_assistant.tar hiking_assistant:latest

View File

@ -12,7 +12,8 @@
## Installation
```bash
uv sync
uv run streamlit run hiking_assistant/main.py
cd hiking_assistant
uv run streamlit run app.py
```
## Dirs

200
hiking_assistant/app.py Normal file
View File

@ -0,0 +1,200 @@
# app.py
#
# author: deng
# date: 20251127
import streamlit as st
import yaml
from elevation_profile import ElevationProfileRenderer
from gpx import GPXProcessor
from map_render import MapRenderer
from streamlit_folium import st_folium
from weather import WeatherFetcher
class HikingAssistant:
def __init__(self):
self._config = self._load_config()
def _load_config(self):
with open('assets/config.yaml', 'r') as f:
return yaml.safe_load(f)
def run(self):
"""Hiking Assistant application"""
# Page configuration
st.set_page_config(
page_title=self._config['app']['page_title'],
page_icon=self._config['app']['page_favicon_path'],
layout='wide',
initial_sidebar_state='collapsed',
)
# Title and description
st.title('⛰️ ' + self._config['app']['page_title'])
# File uploader
uploaded_file = st.file_uploader(
'上傳您的 GPX 檔案開始分析路線!',
type=['gpx'],
help='可以從[健行筆記](https://hiking.biji.co/index.php?q=trail&act=gpx_list)、'
'[Hikingbook](https://zh-tw.hikingbook.net/explore/trails?regions=Taiwan)等網站下載 GPX 檔案',
)
if uploaded_file is not None:
try:
# Process GPX file
with st.spinner('正在解析 GPX 檔案...'):
gpx_processor = GPXProcessor(uploaded_file)
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()
# Display statistics section
st.header('📊 路線五四三')
col1, col2, col3, col4, col5, col6 = st.columns(6)
with col1:
st.metric(label='總距離', value=f'{total_distance:.1f}km')
with col2:
st.metric(label='總爬升', value=f'{elevation_gain:.0f}m')
with col3:
st.metric(label='總下降', value=f'{elevation_loss:.0f}m')
with col4:
st.metric(label='最高海拔', value=f'{max_elevation:.0f}m')
with col5:
st.metric(label='最低海拔', value=f'{min_elevation:.0f}m')
with col6:
st.metric(
label='預估行進時間',
value=f'{estimated_time // 60}h {estimated_time % 60}m',
help='依據[Naismiths Rule](https://en.wikipedia.org/wiki/Naismith%27s_rule)計算',
)
# Map section
with st.spinner('正在渲染地圖...'):
map_renderer = MapRenderer()
route_map = map_renderer.create_route_map(all_points, start_point, end_point)
if route_map:
st_folium(route_map, width=None, height=500, key='route_map', returned_objects=[])
else:
st.error('無法渲染地圖')
# Create two columns: elevation profile on left, pie chart on right
profile_col, pie_col = st.columns([2, 1])
with profile_col:
with st.spinner('正在繪製海拔剖面圖...'):
profile_renderer = ElevationProfileRenderer()
elevation_fig = profile_renderer.create_elevation_profile(distances, elevations, gradients)
if elevation_fig:
# elevation_fig = profile_renderer.add_gradient_legend(elevation_fig)
st.plotly_chart(elevation_fig, width='stretch')
else:
st.error('無法繪製海拔剖面圖')
with pie_col:
with st.spinner('正在分析坡度分布...'):
pie_fig = profile_renderer.create_gradient_pie_chart(distances, gradients)
if pie_fig:
st.plotly_chart(pie_fig, width='stretch')
else:
st.error('無法繪製坡度分布圖')
st.divider()
# Weather section
st.header('☀️ 天氣安抓')
if start_point:
with st.spinner('正在獲取天氣資訊...'):
weather_fetcher = WeatherFetcher()
weather_data = weather_fetcher.get_weather(start_point[0], start_point[1])
if weather_data:
weather_info = weather_fetcher.format_weather_display(weather_data)
if weather_info:
# Current weather
st.subheader('此刻起點天氣')
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
st.metric(label='🌡️ 溫度', value=weather_info['temperature'])
with col2:
st.metric(label='💧 濕度', value=weather_info['humidity'])
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')
with col4:
st.metric(label='🌧️ 降雨量', value=weather_info['precipitation'])
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)
# 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)
st.metric(
label='💨 風速與流向',
value=f'{weather_info["wind_speed"]} {wind_dir_text} {wind_level}',
)
# Forecast
if weather_info['forecast']:
st.subheader('未來三天預報')
forecast_cols = st.columns(3)
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('⚠️ 無法格式化天氣資料')
else:
st.warning('⚠️ 無法獲取天氣資訊,請稍後再試')
else:
st.warning('⚠️ 無法確定起點位置')
except Exception as e:
st.error(f'❌ 處理檔案時發生錯誤: {str(e)}')
st.divider()
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)
if __name__ == '__main__':
app = HikingAssistant()
app.run()

View File

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

View File

@ -8,7 +8,44 @@ class ElevationProfileRenderer:
def __init__(self):
"""Initialize elevation profile renderer."""
pass
self.color = {
'steep_ascent': 'rgb(139, 69, 19)',
'gentle_ascent': 'rgb(210, 180, 140)',
'steep_descent': 'rgb(34, 139, 34)',
'gentle_descent': 'rgb(154, 205, 50)',
}
self.slope = {
'steep_ascent': [15, float('inf')],
'gentle_ascent': [0, 15],
'steep_descent': [float('-inf'), -20],
'gentle_descent': [-20, 0],
}
self.label = {
'steep_ascent': '陡上坡 (>= 15%)',
'gentle_ascent': '緩上坡 (< 15%)',
'steep_descent': '陡下坡 (<= -20%)',
'gentle_descent': '緩下坡 (> -20%)',
}
def _get_gradient_category(self, gradient):
"""Determine gradient category based on slope thresholds.
Args:
gradient: Gradient value in percentage
Returns:
str: Category key ('steep_ascent', 'gentle_ascent', 'steep_descent', 'gentle_descent')
"""
if gradient >= self.slope['steep_ascent'][0]:
return 'steep_ascent'
elif gradient >= self.slope['gentle_ascent'][0]:
return 'gentle_ascent'
elif gradient <= self.slope['steep_descent'][1]:
return 'steep_descent'
else:
return 'gentle_descent'
def _downsample_data(self, distances, elevations, gradients, max_points=800):
"""Downsample data if there are too many points.
@ -84,23 +121,10 @@ class ElevationProfileRenderer:
while i < len(distances) - 1:
gradient = gradients_extended[i + 1]
# Determine color based on gradient thresholds (earth-tone palette)
if gradient > 0: # Ascending
if gradient >= 15:
# Steep ascent: dark red-brown (brick/terracotta)
color = 'rgb(139, 69, 19)' # Saddle brown
else:
# Gentle ascent: light red-brown (sandy/tan)
color = 'rgb(210, 180, 140)' # Tan
is_ascending = True
else: # Descending
if gradient <= -20:
# Steep descent: dark green (forest green)
color = 'rgb(34, 139, 34)' # Forest green
else:
# Gentle descent: light green (sage/olive)
color = 'rgb(154, 205, 50)' # Yellow green
is_ascending = False
# Determine color based on gradient category
category = self._get_gradient_category(gradient)
color = self.color[category]
is_ascending = gradient > 0
# Collect consecutive points with similar direction (ascending/descending)
segment_x = [distances[i]]
@ -179,14 +203,15 @@ class ElevationProfileRenderer:
if i + 1 < len(distances):
segment_distance = distances[i + 1] - distances[i]
gradient = gradients[i]
category = self._get_gradient_category(gradient)
if gradient >= 15:
if category == 'steep_ascent':
steep_ascent_km += segment_distance
elif gradient > 0:
elif category == 'gentle_ascent':
gentle_ascent_km += segment_distance
elif gradient <= -20:
elif category == 'steep_descent':
steep_descent_km += segment_distance
else: # gradient < 0 and > -20
else: # gentle_descent
gentle_descent_km += segment_distance
return {
@ -211,33 +236,23 @@ class ElevationProfileRenderer:
if not stats:
return None
# Prepare data
labels = ['陡上坡 (≥15%)', '緩上坡 (<15%)', '陡下坡 (≤-20%)', '緩下坡 (>-20%)']
values = [stats['steep_ascent'], stats['gentle_ascent'], stats['steep_descent'], stats['gentle_descent']]
colors = [
'rgb(139, 69, 19)', # Steep ascent - dark brown
'rgb(210, 180, 140)', # Gentle ascent - tan
'rgb(34, 139, 34)', # Steep descent - forest green
'rgb(154, 205, 50)', # Gentle descent - yellow green
]
# Create pie chart
fig = go.Figure(
data=[
go.Pie(
labels=labels,
values=values,
marker=dict(colors=colors),
labels=list(self.label.values()),
values=list(stats.values()),
marker=dict(colors=list(self.color.values())),
textinfo='label+percent',
textposition='inside',
hovertemplate='%{label}<br>%{value:.2f} km<br>%{percent}<extra></extra>',
hovertemplate='%{value:.2f} km<extra></extra>',
showlegend=False, # Don't show legend as requested
)
]
)
fig.update_layout(
title='坡度分布',
title='',
height=400,
margin=dict(l=20, r=20, t=40, b=20),
)
@ -259,8 +274,8 @@ class ElevationProfileRenderer:
x=[None],
y=[None],
mode='lines',
line=dict(color='rgb(139,69,19)', width=3),
name='陡上坡 (≥15%)',
line=dict(color=self.color['steep_ascent'], width=3),
name=self.label['steep_ascent'],
)
)
@ -269,8 +284,8 @@ class ElevationProfileRenderer:
x=[None],
y=[None],
mode='lines',
line=dict(color='rgb(210,180,140)', width=3),
name='緩上坡 (<15%)',
line=dict(color=self.color['gentle_ascent'], width=3),
name=self.label['gentle_ascent'],
)
)
@ -279,8 +294,8 @@ class ElevationProfileRenderer:
x=[None],
y=[None],
mode='lines',
line=dict(color='rgb(34,139,34)', width=3),
name='陡下坡 (≤-20%)',
line=dict(color=self.color['steep_descent'], width=3),
name=self.label['steep_descent'],
)
)
@ -289,8 +304,8 @@ class ElevationProfileRenderer:
x=[None],
y=[None],
mode='lines',
line=dict(color='rgb(154,205,50)', width=3),
name='緩下坡 (>-20%)',
line=dict(color=self.color['gentle_descent'], width=3),
name=self.label['gentle_descent'],
)
)

View File

@ -1,4 +1,7 @@
"""GPX file processing module for hiking route analysis."""
# gpx.py
#
# author: deng
# date: 20251127
import gpxpy
import gpxpy.gpx
@ -165,3 +168,31 @@ class GPXProcessor:
gradients.append(gradient)
return gradients
def calculate_naismith_time(self):
"""Calculate estimated hiking time using Naismith's Rule.
Naismith's Rule:
- Base time: 1 hour per 5 km of horizontal distance
- Add time: 1 hour per 600 meters of ascent
Returns:
int: Estimated time in minutes
"""
if len(self.points) < 2:
return 0
# Get total distance and elevation gain
total_distance = self.calculate_distance() if not self.distances else self.distances[-1]
elevation_gain, _ = self.calculate_elevation_gain_loss()
# Naismith's Rule calculation
# 1 hour per 5 km = 0.2 hours per km
time_for_distance = total_distance * 0.2
# 1 hour per 600 meters of ascent
time_for_ascent = elevation_gain / 600.0
total_time = int((time_for_distance + time_for_ascent) * 60)
return total_time

View File

@ -1,182 +0,0 @@
"""台灣登山路線規劃輔助系統 - Streamlit Web Application"""
import streamlit as st
from elevation_profile import ElevationProfileRenderer
from gpx import GPXProcessor
from map_render import MapRenderer
from streamlit_folium import st_folium
from weather import WeatherFetcher
def main():
"""Main Streamlit application."""
# Page configuration
st.set_page_config(
page_title='臺灣登山路線分析小幫手',
page_icon='assets/favicon_compressed.jpg',
layout='wide',
initial_sidebar_state='collapsed',
)
# Title and description
st.title('⛰️ 臺灣登山路線分析小幫手')
st.markdown('上傳您的 GPX 檔案開始分析路線!')
# File uploader
uploaded_file = st.file_uploader(
'上傳檔案',
type=['gpx'],
help='請上傳包含登山路線的 GPX 檔案',
)
if uploaded_file is not None:
try:
# Process GPX file
with st.spinner('正在解析 GPX 檔案...'):
gpx_processor = GPXProcessor(uploaded_file)
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()
# Display statistics section
st.header('📊 路線統計資訊')
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
st.metric(label='總距離', value=f'{total_distance:.2f}', delta='公里')
with col2:
st.metric(label='總爬升', value=f'{elevation_gain:.0f}', delta='公尺')
with col3:
st.metric(label='總下降', value=f'{elevation_loss:.0f}', delta='公尺')
with col4:
st.metric(label='最高海拔', value=f'{max_elevation:.0f}', delta='公尺')
with col5:
st.metric(label='最低海拔', value=f'{min_elevation:.0f}', delta='公尺')
st.divider()
# Map and Elevation profile section (no divider between them)
st.header('🗺️ 路線地圖')
# Map section
with st.spinner('正在渲染地圖...'):
map_renderer = MapRenderer()
route_map = map_renderer.create_route_map(all_points, start_point, end_point)
if route_map:
st_folium(route_map, width=None, height=500)
else:
st.error('無法渲染地圖')
# Elevation profile section (directly below map, no divider)
# st.markdown('🔴 **紅色**表示上升路段(顏色越深坡度越陡) | 🟢 **綠色**表示下降路段(顏色越深坡度越陡)')
# Create two columns: elevation profile on left, pie chart on right
profile_col, pie_col = st.columns([2, 1])
with profile_col:
with st.spinner('正在繪製海拔剖面圖...'):
profile_renderer = ElevationProfileRenderer()
elevation_fig = profile_renderer.create_elevation_profile(distances, elevations, gradients)
if elevation_fig:
# Add legend
elevation_fig = profile_renderer.add_gradient_legend(elevation_fig)
st.plotly_chart(elevation_fig, width='stretch')
else:
st.error('無法繪製海拔剖面圖')
with pie_col:
with st.spinner('正在分析坡度分布...'):
pie_fig = profile_renderer.create_gradient_pie_chart(distances, gradients)
if pie_fig:
st.plotly_chart(pie_fig, width='stretch')
else:
st.error('無法繪製坡度分布圖')
st.divider()
# Weather section
st.header('☀️ 起點天氣預報')
if start_point:
with st.spinner('正在獲取天氣資訊...'):
weather_fetcher = WeatherFetcher()
weather_data = weather_fetcher.get_weather(start_point[0], start_point[1])
if weather_data:
weather_info = weather_fetcher.format_weather_display(weather_data)
if weather_info:
# Current weather
st.subheader('當前天氣')
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(label='🌡️ 溫度', value=weather_info['temperature'])
with col2:
st.metric(label='💧 濕度', value=weather_info['humidity'])
with col3:
st.metric(label='🌧️ 降雨量', value=weather_info['precipitation'])
with col4:
st.metric(label='💨 風速', value=weather_info['wind_speed'])
# Forecast
if weather_info['forecast']:
st.subheader('未來三天預報')
forecast_cols = st.columns(3)
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('⚠️ 無法格式化天氣資料')
else:
st.warning('⚠️ 無法獲取天氣資訊,請稍後再試')
else:
st.warning('⚠️ 無法確定起點位置')
except Exception as e:
st.error(f'❌ 處理檔案時發生錯誤: {str(e)}')
else:
# # Show instructions when no file is uploaded
# st.info('👆 請上傳 GPX 檔案開始分析')
# st.markdown(
# """
# ### 使用說明
# 1. 點擊上方的「選擇 GPX 檔案」按鈕
# 2. 選擇您的登山路線 GPX 檔案
# 3. 系統將自動分析並顯示:
# - 📊 路線統計資訊(距離、爬升、海拔等)
# - 🗺️ 互動式路線地圖
# - 📈 海拔剖面圖(顏色顯示坡度變化)
# - ☀️ 起點天氣預報
# """
# )
pass
if __name__ == '__main__':
main()

View File

@ -1,4 +1,7 @@
"""Map rendering module using Folium for interactive maps."""
# map_render.py
#
# author: deng
# date: 20251127
import folium

View File

@ -1,4 +1,7 @@
"""Weather data fetching module using Open-Meteo API."""
# weather.py
#
# author: deng
# date: 20251127
import requests
@ -8,7 +11,44 @@ class WeatherFetcher:
def __init__(self):
"""Initialize weather fetcher."""
self.base_url = 'https://api.open-meteo.com/v1/forecast'
self.api_url = 'https://api.open-meteo.com/v1/forecast'
self.request_timeout = 8 # unit: second
def convert_wind_degrees_to_flow_direction(self, degrees):
"""Convert wind direction from degrees to flow direction emoji.
Args:
degrees: Wind direction in degrees (0-360)
Returns:
str: Flow direction as emoji arrow (pointing where wind is blowing to)
"""
if degrees is None:
return 'N/A'
directions = ['⬇️', '↙️', '⬅️', '↖️', '⬆️', '↗️', '➡️', '↘️']
index = round(degrees / 45) % 8
return directions[index]
def get_wind_speed_indicator(self, wind_speed):
"""Get wind speed level indicator based on speed.
Args:
wind_speed: Wind speed in km/h
Returns:
str: Emoji indicator (🟢/🟡/🟠/🔴)
"""
if wind_speed is None:
return ''
if wind_speed < 20:
return '🟢' # Safe
elif wind_speed < 40:
return '🟡' # Caution
elif wind_speed < 60:
return '🟠' # Alert
else:
return '🔴' # Dangerous
def get_weather(self, latitude, longitude):
"""Fetch weather data for given coordinates.
@ -25,18 +65,18 @@ class WeatherFetcher:
params = {
'latitude': latitude,
'longitude': longitude,
'current': 'temperature_2m,relative_humidity_2m,precipitation,wind_speed_10m',
'current': 'temperature_2m,relative_humidity_2m,precipitation,wind_speed_10m,wind_direction_10m',
'daily': 'temperature_2m_max,temperature_2m_min,precipitation_probability_max',
'timezone': 'Asia/Taipei',
'timezone': 'auto',
'forecast_days': 3,
}
response = requests.get(self.base_url, params=params, timeout=10)
response = requests.get(self.api_url, params=params, timeout=self.request_timeout)
response.raise_for_status()
data = response.json()
# Parse current weather
# Parse weather data
current = data.get('current', {})
daily = data.get('daily', {})
@ -44,7 +84,9 @@ class WeatherFetcher:
'current_temperature': current.get('temperature_2m'),
'current_humidity': current.get('relative_humidity_2m'),
'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_direction': current.get('wind_direction_10m'),
'daily_temp_max': daily.get('temperature_2m_max', []),
'daily_temp_min': daily.get('temperature_2m_min', []),
'daily_precipitation_prob': daily.get('precipitation_probability_max', []),

View File

@ -13,6 +13,7 @@ dependencies = [
"requests>=2.32.3",
"geopy>=2.4.1",
"streamlit-plotly-events>=0.0.6",
"pyyaml>=6.0.3",
]
[dependency-groups]

View File

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Hiking Assistant Test"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>合歡山主峰測試路線</name>
<desc>從合歡山主峰登山口到山頂的測試路線</desc>
</metadata>
<trk>
<name>合歡山主峰路線</name>
<trkseg>
<trkpt lat="24.1400" lon="121.2720">
<ele>3150</ele>
</trkpt>
<trkpt lat="24.1405" lon="121.2725">
<ele>3180</ele>
</trkpt>
<trkpt lat="24.1410" lon="121.2730">
<ele>3210</ele>
</trkpt>
<trkpt lat="24.1415" lon="121.2735">
<ele>3250</ele>
</trkpt>
<trkpt lat="24.1420" lon="121.2740">
<ele>3290</ele>
</trkpt>
<trkpt lat="24.1425" lon="121.2745">
<ele>3320</ele>
</trkpt>
<trkpt lat="24.1430" lon="121.2750">
<ele>3350</ele>
</trkpt>
<trkpt lat="24.1435" lon="121.2755">
<ele>3360</ele>
</trkpt>
<trkpt lat="24.1438" lon="121.2758">
<ele>3380</ele>
</trkpt>
<trkpt lat="24.1440" lon="121.2760">
<ele>3400</ele>
</trkpt>
<trkpt lat="24.1442" lon="121.2762">
<ele>3410</ele>
</trkpt>
<trkpt lat="24.1444" lon="121.2764">
<ele>3416</ele>
</trkpt>
<trkpt lat="24.1445" lon="121.2765">
<ele>3417</ele>
</trkpt>
</trkseg>
</trk>
</gpx>

2
uv.lock generated
View File

@ -234,6 +234,7 @@ dependencies = [
{ name = "geopy" },
{ name = "gpxpy" },
{ name = "plotly" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "streamlit" },
{ name = "streamlit-folium" },
@ -253,6 +254,7 @@ requires-dist = [
{ name = "geopy", specifier = ">=2.4.1" },
{ name = "gpxpy", specifier = ">=1.6.2" },
{ name = "plotly", specifier = ">=5.24.1" },
{ name = "pyyaml", specifier = ">=6.0.3" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "streamlit", specifier = ">=1.51.0" },
{ name = "streamlit-folium", specifier = ">=0.23.1" },