Compare commits
3 Commits
74e1d96600
...
1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| f7b79028a6 | |||
| 302e105f09 | |||
| 145b52490f |
@ -1,5 +1,14 @@
|
|||||||
# 更新紀錄
|
# 更新紀錄
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-05-25
|
||||||
|
|
||||||
|
### ⚡ 效能優化
|
||||||
|
|
||||||
|
- 將設定檔格式從 YAML 改為 TOML,改用 Python 內建 `tomllib` 解析,減少第三方依賴並加快啟動速度
|
||||||
|
- 在背景執行緒預熱重量級模組(`plotly`、`folium`、`gpxpy`、`numpy`、`geopy`),縮短首次載入頁面時間
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.0.0] - 2025-12-03
|
## [1.0.0] - 2025-12-03
|
||||||
|
|
||||||
### 🎉 首次正式發布
|
### 🎉 首次正式發布
|
||||||
|
|||||||
@ -47,5 +47,5 @@ EXPOSE 8800
|
|||||||
CMD ["python", "-m", "streamlit", "run", "app.py"]
|
CMD ["python", "-m", "streamlit", "run", "app.py"]
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
# docker build --platform=linux/amd64 -t hiking_assistant:1.0.0 .
|
# docker build --platform=linux/amd64 -t hiking_assistant:1.0.2 .
|
||||||
# docker save -o hiking_assistant.tar hiking_assistant:1.0.0
|
# docker save -o hiking_assistant.tar hiking_assistant:1.0.2
|
||||||
@ -3,27 +3,26 @@
|
|||||||
# author: deng
|
# author: deng
|
||||||
# date: 20251127
|
# date: 20251127
|
||||||
|
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import yaml
|
from utils import convert_image_to_base64, parse_config
|
||||||
from elevation import ElevationRenderer
|
|
||||||
from gpx import GPXProcessor
|
|
||||||
from map import MapRenderer
|
def _prewarm_imports() -> None:
|
||||||
from streamlit_folium import st_folium
|
"""Pre-import heavy modules in background."""
|
||||||
from utils import convert_image_to_base64
|
import elevation # noqa: F401 (imports plotly)
|
||||||
from weather import WeatherFetcher
|
import gpx # noqa: F401 (imports gpxpy, numpy, geopy)
|
||||||
|
import map # noqa: F401 (imports folium)
|
||||||
|
import weather # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
class HikingAssistant:
|
class HikingAssistant:
|
||||||
def __init__(self):
|
def __init__(self, config_path: str = 'assets/config.toml') -> None:
|
||||||
self._config = self._load_config()
|
self._config = parse_config(config_path)
|
||||||
|
|
||||||
def _load_config(self):
|
def _set_page_background(self, image_path: str, opacity: float = 0.8) -> None:
|
||||||
with open('assets/config.yaml', 'r') as f:
|
|
||||||
return yaml.safe_load(f)
|
|
||||||
|
|
||||||
def _set_page_background(self, image_path, opacity=0.8):
|
|
||||||
"""Set background image for the application.
|
"""Set background image for the application.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -46,7 +45,7 @@ class HikingAssistant:
|
|||||||
unsafe_allow_html=True,
|
unsafe_allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _add_custom_title(self, title, image_path):
|
def _add_custom_title(self, title: str, image_path: str) -> None:
|
||||||
favicon_data = convert_image_to_base64(image_path)
|
favicon_data = convert_image_to_base64(image_path)
|
||||||
ext = image_path.split('.')[-1]
|
ext = image_path.split('.')[-1]
|
||||||
st.markdown(
|
st.markdown(
|
||||||
@ -59,7 +58,7 @@ class HikingAssistant:
|
|||||||
unsafe_allow_html=True,
|
unsafe_allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
"""Hiking Assistant application"""
|
"""Hiking Assistant application"""
|
||||||
# Page configuration
|
# Page configuration
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
@ -93,6 +92,8 @@ class HikingAssistant:
|
|||||||
if 'gpx_file_key' not in st.session_state or st.session_state.gpx_file_key != file_key:
|
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)
|
# Process GPX file (only when it's a new file)
|
||||||
with st.spinner('正在解析 GPX 檔案...'):
|
with st.spinner('正在解析 GPX 檔案...'):
|
||||||
|
from gpx import GPXProcessor
|
||||||
|
|
||||||
gpx_processor = GPXProcessor(uploaded_file)
|
gpx_processor = GPXProcessor(uploaded_file)
|
||||||
|
|
||||||
if not gpx_processor.validate_and_parse():
|
if not gpx_processor.validate_and_parse():
|
||||||
@ -176,6 +177,9 @@ class HikingAssistant:
|
|||||||
|
|
||||||
# Map section
|
# Map section
|
||||||
with st.spinner('正在渲染地圖...'):
|
with st.spinner('正在渲染地圖...'):
|
||||||
|
from map import MapRenderer
|
||||||
|
from streamlit_folium import st_folium
|
||||||
|
|
||||||
map_renderer = MapRenderer()
|
map_renderer = MapRenderer()
|
||||||
route_map = map_renderer.create_route_map(all_points, start_point, end_point, waypoints)
|
route_map = map_renderer.create_route_map(all_points, start_point, end_point, waypoints)
|
||||||
|
|
||||||
@ -189,6 +193,8 @@ class HikingAssistant:
|
|||||||
|
|
||||||
with profile_col:
|
with profile_col:
|
||||||
with st.spinner('正在繪製海拔剖面圖...'):
|
with st.spinner('正在繪製海拔剖面圖...'):
|
||||||
|
from elevation import ElevationRenderer
|
||||||
|
|
||||||
profile_renderer = ElevationRenderer()
|
profile_renderer = ElevationRenderer()
|
||||||
elevation_fig = profile_renderer.create_elevation_profile(distances, elevations, gradients)
|
elevation_fig = profile_renderer.create_elevation_profile(distances, elevations, gradients)
|
||||||
|
|
||||||
@ -213,6 +219,8 @@ class HikingAssistant:
|
|||||||
st.header('☀️ 天氣安抓')
|
st.header('☀️ 天氣安抓')
|
||||||
|
|
||||||
if start_point:
|
if start_point:
|
||||||
|
from weather import WeatherFetcher
|
||||||
|
|
||||||
weather_fetcher = WeatherFetcher(forecast_days=self._config['app']['weather']['forecast_days'])
|
weather_fetcher = WeatherFetcher(forecast_days=self._config['app']['weather']['forecast_days'])
|
||||||
weather_key = f'weather_{start_point[0]:.6f}_{start_point[1]:.6f}'
|
weather_key = f'weather_{start_point[0]:.6f}_{start_point[1]:.6f}'
|
||||||
|
|
||||||
@ -343,6 +351,12 @@ class HikingAssistant:
|
|||||||
st.markdown(footer_text, unsafe_allow_html=True)
|
st.markdown(footer_text, unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
|
@st.cache_resource(show_spinner=False, max_entries=1)
|
||||||
|
def get_app_instance() -> HikingAssistant:
|
||||||
|
return HikingAssistant()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = HikingAssistant()
|
threading.Thread(target=_prewarm_imports, daemon=True).start()
|
||||||
|
app = get_app_instance()
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
22
hiking_assistant/assets/config.toml
Normal file
22
hiking_assistant/assets/config.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[app]
|
||||||
|
version = "1.0.2"
|
||||||
|
page_title = "山山登山小助手"
|
||||||
|
page_favicon_path = "./assets/new_favicon.webp"
|
||||||
|
page_footer_text = "⚠️ 本服務提供之資訊僅供規劃參考,山區氣候瞬息萬變,請務必依據現場狀況與自身能力進行風險評估<br>Made with ❤️ by <a href=\"https://gitea.guineapig.love/deng\">deng</a>"
|
||||||
|
page_background_path = "./assets/background_compressed.jpg"
|
||||||
|
page_background_opacity = 0.8
|
||||||
|
|
||||||
|
[app.altitude_sickness]
|
||||||
|
elevation_threshold = 2100
|
||||||
|
warning_text = "此路線海拔較高,請留意[高山症](https://www.ysnp.gov.tw/StaticPage/MountainSickness)發生風險"
|
||||||
|
emoji = "💊"
|
||||||
|
|
||||||
|
[app.estimated_time]
|
||||||
|
horizontal_speed = 3
|
||||||
|
vertical_speed = 400
|
||||||
|
|
||||||
|
[app.map]
|
||||||
|
height = 550
|
||||||
|
|
||||||
|
[app.weather]
|
||||||
|
forecast_days = 7
|
||||||
@ -3,13 +3,15 @@
|
|||||||
# author: deng
|
# author: deng
|
||||||
# date: 20251127
|
# date: 20251127
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
|
||||||
class ElevationRenderer:
|
class ElevationRenderer:
|
||||||
"""Render elevation profiles with gradient-based coloring."""
|
"""Render elevation profiles with gradient-based coloring."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize elevation profile renderer."""
|
"""Initialize elevation profile renderer."""
|
||||||
self.color = {
|
self.color = {
|
||||||
'steep_ascent': 'rgb(139, 69, 19)',
|
'steep_ascent': 'rgb(139, 69, 19)',
|
||||||
@ -32,7 +34,7 @@ class ElevationRenderer:
|
|||||||
'gentle_descent': '緩下坡 (> -20%)',
|
'gentle_descent': '緩下坡 (> -20%)',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_gradient_category(self, gradient):
|
def _get_gradient_category(self, gradient: float) -> str:
|
||||||
"""Determine gradient category based on slope thresholds.
|
"""Determine gradient category based on slope thresholds.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -50,7 +52,9 @@ class ElevationRenderer:
|
|||||||
else:
|
else:
|
||||||
return 'gentle_descent'
|
return 'gentle_descent'
|
||||||
|
|
||||||
def _downsample_data(self, distances, elevations, gradients, max_points=800):
|
def _downsample_data(
|
||||||
|
self, distances: list[float], elevations: list[float], gradients: list[float], max_points: int = 800
|
||||||
|
) -> tuple[list[float], list[float], list[float]]:
|
||||||
"""Downsample data if there are too many points.
|
"""Downsample data if there are too many points.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -96,7 +100,7 @@ class ElevationRenderer:
|
|||||||
|
|
||||||
return downsampled_distances, downsampled_elevations, downsampled_gradients
|
return downsampled_distances, downsampled_elevations, downsampled_gradients
|
||||||
|
|
||||||
def create_elevation_profile(self, distances, elevations, gradients):
|
def create_elevation_profile(self, distances: list[float], elevations: list[float], gradients: list[float]) -> Optional[go.Figure]:
|
||||||
"""Create an interactive elevation profile chart.
|
"""Create an interactive elevation profile chart.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -184,7 +188,7 @@ class ElevationRenderer:
|
|||||||
|
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
def calculate_gradient_distribution(self, distances, gradients):
|
def calculate_gradient_distribution(self, distances: list[float], gradients: list[float]) -> Optional[dict[str, float]]:
|
||||||
"""Calculate distance distribution across different gradient categories.
|
"""Calculate distance distribution across different gradient categories.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -224,7 +228,7 @@ class ElevationRenderer:
|
|||||||
'gentle_descent': gentle_descent_km,
|
'gentle_descent': gentle_descent_km,
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_gradient_pie_chart(self, distances, gradients):
|
def create_gradient_pie_chart(self, distances: list[float], gradients: list[float]) -> Optional[go.Figure]:
|
||||||
"""Create a pie chart showing gradient distribution.
|
"""Create a pie chart showing gradient distribution.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -262,7 +266,7 @@ class ElevationRenderer:
|
|||||||
|
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
def add_gradient_legend(self, fig):
|
def add_gradient_legend(self, fig: go.Figure) -> go.Figure:
|
||||||
"""Add a custom legend explaining the color scheme.
|
"""Add a custom legend explaining the color scheme.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
# author: deng
|
# author: deng
|
||||||
# date: 20251127
|
# date: 20251127
|
||||||
|
|
||||||
|
from typing import IO, Optional
|
||||||
|
|
||||||
import gpxpy
|
import gpxpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from geopy.distance import geodesic
|
from geopy.distance import geodesic
|
||||||
@ -11,7 +13,7 @@ from geopy.distance import geodesic
|
|||||||
class GPXProcessor:
|
class GPXProcessor:
|
||||||
"""Handle GPX file parsing and route analysis."""
|
"""Handle GPX file parsing and route analysis."""
|
||||||
|
|
||||||
def __init__(self, gpx_file):
|
def __init__(self, gpx_file: IO) -> None:
|
||||||
"""Initialize GPX processor with uploaded file.
|
"""Initialize GPX processor with uploaded file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -25,7 +27,7 @@ class GPXProcessor:
|
|||||||
self.distances = [] # unit: km
|
self.distances = [] # unit: km
|
||||||
self.waypoints = [] # list of (lat, lon, name, elevation)
|
self.waypoints = [] # list of (lat, lon, name, elevation)
|
||||||
|
|
||||||
def _get_sample_rate(self):
|
def _get_sample_rate(self) -> float:
|
||||||
"""Get the median time interval between points.
|
"""Get the median time interval between points.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -37,7 +39,7 @@ class GPXProcessor:
|
|||||||
median_sample_time = np.median(diff).total_seconds()
|
median_sample_time = np.median(diff).total_seconds()
|
||||||
return median_sample_time
|
return median_sample_time
|
||||||
|
|
||||||
def validate_and_parse(self):
|
def validate_and_parse(self) -> bool:
|
||||||
"""Validate and parse the GPX file.
|
"""Validate and parse the GPX file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -77,7 +79,7 @@ class GPXProcessor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f'GPX 檔案解析失敗: {str(e)}')
|
raise Exception(f'GPX 檔案解析失敗: {str(e)}')
|
||||||
|
|
||||||
def calculate_distance(self):
|
def calculate_distance(self) -> float:
|
||||||
"""Calculate total distance of the route.
|
"""Calculate total distance of the route.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -96,7 +98,7 @@ class GPXProcessor:
|
|||||||
|
|
||||||
return total_distance
|
return total_distance
|
||||||
|
|
||||||
def calculate_elevation_gain_loss(self, threshold=1.0):
|
def calculate_elevation_gain_loss(self, threshold: float = 1.0) -> tuple[float, float]:
|
||||||
"""Calculate total elevation gain and loss with noise filtering.
|
"""Calculate total elevation gain and loss with noise filtering.
|
||||||
|
|
||||||
GPS altitude data is notoriously inaccurate. Small fluctuations (noise) can
|
GPS altitude data is notoriously inaccurate. Small fluctuations (noise) can
|
||||||
@ -127,7 +129,7 @@ class GPXProcessor:
|
|||||||
|
|
||||||
return gain, loss
|
return gain, loss
|
||||||
|
|
||||||
def get_min_max_elevation(self):
|
def get_min_max_elevation(self) -> tuple[float, float]:
|
||||||
"""Get minimum and maximum elevation.
|
"""Get minimum and maximum elevation.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -138,7 +140,7 @@ class GPXProcessor:
|
|||||||
|
|
||||||
return min(self.elevations), max(self.elevations)
|
return min(self.elevations), max(self.elevations)
|
||||||
|
|
||||||
def get_start_end_points(self):
|
def get_start_end_points(self) -> tuple[Optional[tuple[float, float, float]], Optional[tuple[float, float, float]]]:
|
||||||
"""Get start and end point coordinates with elevation.
|
"""Get start and end point coordinates with elevation.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -149,7 +151,7 @@ class GPXProcessor:
|
|||||||
|
|
||||||
return (self.points[0][0], self.points[0][1], self.elevations[0]), (self.points[-1][0], self.points[-1][1], self.elevations[-1])
|
return (self.points[0][0], self.points[0][1], self.elevations[0]), (self.points[-1][0], self.points[-1][1], self.elevations[-1])
|
||||||
|
|
||||||
def get_all_points(self):
|
def get_all_points(self) -> list[tuple[float, float]]:
|
||||||
"""Get all route points.
|
"""Get all route points.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -157,7 +159,7 @@ class GPXProcessor:
|
|||||||
"""
|
"""
|
||||||
return self.points
|
return self.points
|
||||||
|
|
||||||
def get_elevation_profile_data(self):
|
def get_elevation_profile_data(self) -> tuple[list[float], list[float]]:
|
||||||
"""Get data for elevation profile visualization.
|
"""Get data for elevation profile visualization.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -165,7 +167,7 @@ class GPXProcessor:
|
|||||||
"""
|
"""
|
||||||
return self.distances, self.elevations
|
return self.distances, self.elevations
|
||||||
|
|
||||||
def get_gradients(self):
|
def get_gradients(self) -> list[float]:
|
||||||
"""Calculate gradient (slope) for each segment.
|
"""Calculate gradient (slope) for each segment.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -188,7 +190,7 @@ class GPXProcessor:
|
|||||||
|
|
||||||
return gradients
|
return gradients
|
||||||
|
|
||||||
def calculate_naismith_time(self, horizontal_speed=5, vertical_speed=600):
|
def calculate_naismith_time(self, horizontal_speed: float = 5, vertical_speed: float = 600) -> int:
|
||||||
"""Calculate estimated hiking time using Naismith's Rule.
|
"""Calculate estimated hiking time using Naismith's Rule.
|
||||||
|
|
||||||
Naismith's Rule:
|
Naismith's Rule:
|
||||||
|
|||||||
@ -3,17 +3,26 @@
|
|||||||
# author: deng
|
# author: deng
|
||||||
# date: 20251127
|
# date: 20251127
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import folium
|
import folium
|
||||||
|
|
||||||
|
|
||||||
class MapRenderer:
|
class MapRenderer:
|
||||||
"""Render hiking routes on interactive maps."""
|
"""Render hiking routes on interactive maps."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize map renderer."""
|
"""Initialize map renderer."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create_route_map(self, points, start_point, end_point, waypoints=None, tile_layer='OpenStreetMap'):
|
def create_route_map(
|
||||||
|
self,
|
||||||
|
points: list[tuple[float, float]],
|
||||||
|
start_point: Optional[tuple[float, float, float]],
|
||||||
|
end_point: Optional[tuple[float, float, float]],
|
||||||
|
waypoints: Optional[list[dict]] = None,
|
||||||
|
tile_layer: str = 'OpenStreetMap',
|
||||||
|
) -> Optional[folium.Map]:
|
||||||
"""Create an interactive map with the hiking route.
|
"""Create an interactive map with the hiking route.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -117,7 +126,7 @@ class MapRenderer:
|
|||||||
|
|
||||||
return route_map
|
return route_map
|
||||||
|
|
||||||
def add_hover_marker(self, route_map, position, label='當前位置'):
|
def add_hover_marker(self, route_map: folium.Map, position: tuple[float, float], label: str = '當前位置') -> folium.Map:
|
||||||
"""Add a hover position marker to the map.
|
"""Add a hover position marker to the map.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@ -4,11 +4,26 @@
|
|||||||
# date: 20251128
|
# date: 20251128
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import tomllib
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
|
|
||||||
@st.cache_data
|
@st.cache_data
|
||||||
def convert_image_to_base64(image_path):
|
def convert_image_to_base64(image_path: str) -> str:
|
||||||
with open(image_path, 'rb') as f:
|
with open(image_path, 'rb') as f:
|
||||||
return base64.b64encode(f.read()).decode()
|
return base64.b64encode(f.read()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config(config_path: str) -> dict:
|
||||||
|
"""Config parser
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path (str): path of config toml.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: configuration dictionary
|
||||||
|
"""
|
||||||
|
with open(config_path, 'rb') as file:
|
||||||
|
config = tomllib.load(file)
|
||||||
|
return config
|
||||||
|
|||||||
@ -3,19 +3,21 @@
|
|||||||
# author: deng
|
# author: deng
|
||||||
# date: 20251127
|
# date: 20251127
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
class WeatherFetcher:
|
class WeatherFetcher:
|
||||||
"""Fetch weather data for hiking locations."""
|
"""Fetch weather data for hiking locations."""
|
||||||
|
|
||||||
def __init__(self, api_url='https://api.open-meteo.com/v1/forecast', request_timeout=8, forecast_days=7):
|
def __init__(self, api_url: str = 'https://api.open-meteo.com/v1/forecast', request_timeout: int = 8, forecast_days: int = 7) -> None:
|
||||||
"""Initialize weather fetcher."""
|
"""Initialize weather fetcher."""
|
||||||
self.api_url = api_url
|
self.api_url = api_url
|
||||||
self.request_timeout = request_timeout
|
self.request_timeout = request_timeout
|
||||||
self.forecast_days = max(forecast_days, 1)
|
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: Optional[float]) -> str:
|
||||||
"""Convert wind direction from degrees to flow direction emoji.
|
"""Convert wind direction from degrees to flow direction emoji.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -30,7 +32,7 @@ class WeatherFetcher:
|
|||||||
index = round(degrees / 45) % 8
|
index = round(degrees / 45) % 8
|
||||||
return directions[index]
|
return directions[index]
|
||||||
|
|
||||||
def get_wind_speed_indicator(self, wind_speed):
|
def get_wind_speed_indicator(self, wind_speed: Optional[float]) -> str:
|
||||||
"""Get wind speed level indicator based on speed.
|
"""Get wind speed level indicator based on speed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -51,7 +53,7 @@ class WeatherFetcher:
|
|||||||
else:
|
else:
|
||||||
return '🔴' # Dangerous
|
return '🔴' # Dangerous
|
||||||
|
|
||||||
def get_uv_index_indicator(self, uv_index):
|
def get_uv_index_indicator(self, uv_index: Optional[float]) -> str:
|
||||||
"""Get UV index level indicator based on index.
|
"""Get UV index level indicator based on index.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -74,7 +76,7 @@ class WeatherFetcher:
|
|||||||
else:
|
else:
|
||||||
return '🟣' # Extreme
|
return '🟣' # Extreme
|
||||||
|
|
||||||
def get_weather(self, latitude, longitude):
|
def get_weather(self, latitude: float, longitude: float) -> Optional[dict]:
|
||||||
"""Fetch weather data for given coordinates.
|
"""Fetch weather data for given coordinates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "hiking-assistant"
|
name = "hiking-assistant"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
description = "This is a web app to analyze gpx for hiking planning"
|
description = "This is a web app to analyze gpx for hiking planning"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
@ -13,7 +13,6 @@ dependencies = [
|
|||||||
"requests>=2.32.3",
|
"requests>=2.32.3",
|
||||||
"geopy>=2.4.1",
|
"geopy>=2.4.1",
|
||||||
"streamlit-plotly-events>=0.0.6",
|
"streamlit-plotly-events>=0.0.6",
|
||||||
"pyyaml>=6.0.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
4
uv.lock
generated
4
uv.lock
generated
@ -227,14 +227,13 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hiking-assistant"
|
name = "hiking-assistant"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "folium" },
|
{ name = "folium" },
|
||||||
{ name = "geopy" },
|
{ name = "geopy" },
|
||||||
{ name = "gpxpy" },
|
{ name = "gpxpy" },
|
||||||
{ name = "plotly" },
|
{ name = "plotly" },
|
||||||
{ name = "pyyaml" },
|
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "streamlit" },
|
{ name = "streamlit" },
|
||||||
{ name = "streamlit-folium" },
|
{ name = "streamlit-folium" },
|
||||||
@ -254,7 +253,6 @@ requires-dist = [
|
|||||||
{ name = "geopy", specifier = ">=2.4.1" },
|
{ name = "geopy", specifier = ">=2.4.1" },
|
||||||
{ name = "gpxpy", specifier = ">=1.6.2" },
|
{ name = "gpxpy", specifier = ">=1.6.2" },
|
||||||
{ name = "plotly", specifier = ">=5.24.1" },
|
{ name = "plotly", specifier = ">=5.24.1" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
|
||||||
{ name = "requests", specifier = ">=2.32.3" },
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
{ name = "streamlit", specifier = ">=1.51.0" },
|
{ name = "streamlit", specifier = ">=1.51.0" },
|
||||||
{ name = "streamlit-folium", specifier = ">=0.23.1" },
|
{ name = "streamlit-folium", specifier = ">=0.23.1" },
|
||||||
|
|||||||
Reference in New Issue
Block a user