add annotation

This commit is contained in:
deng
2025-12-10 19:33:52 +08:00
parent 74e1d96600
commit 145b52490f
9 changed files with 56 additions and 38 deletions

View File

@ -47,5 +47,5 @@ EXPOSE 8800
CMD ["python", "-m", "streamlit", "run", "app.py"]
# Build
# docker build --platform=linux/amd64 -t hiking_assistant:1.0.0 .
# docker save -o hiking_assistant.tar hiking_assistant:1.0.0
# docker build --platform=linux/amd64 -t hiking_assistant:1.0.1 .
# docker save -o hiking_assistant.tar hiking_assistant:1.0.1

View File

@ -16,14 +16,15 @@ from weather import WeatherFetcher
class HikingAssistant:
def __init__(self):
self._config = self._load_config()
def __init__(self, config_path: str = 'assets/config.yaml') -> None:
self._config = self._load_config(config_path)
def _load_config(self):
with open('assets/config.yaml', 'r') as f:
@staticmethod
def _load_config(config_path: str) -> dict:
with open(config_path, 'r') as f:
return yaml.safe_load(f)
def _set_page_background(self, image_path, opacity=0.8):
def _set_page_background(self, image_path: str, opacity: float = 0.8) -> None:
"""Set background image for the application.
Args:
@ -46,7 +47,7 @@ class HikingAssistant:
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)
ext = image_path.split('.')[-1]
st.markdown(
@ -59,7 +60,7 @@ class HikingAssistant:
unsafe_allow_html=True,
)
def run(self):
def run(self) -> None:
"""Hiking Assistant application"""
# Page configuration
st.set_page_config(

View File

@ -3,13 +3,15 @@
# author: deng
# date: 20251127
from typing import Optional
import plotly.graph_objects as go
class ElevationRenderer:
"""Render elevation profiles with gradient-based coloring."""
def __init__(self):
def __init__(self) -> None:
"""Initialize elevation profile renderer."""
self.color = {
'steep_ascent': 'rgb(139, 69, 19)',
@ -32,7 +34,7 @@ class ElevationRenderer:
'gentle_descent': '緩下坡 (> -20%)',
}
def _get_gradient_category(self, gradient):
def _get_gradient_category(self, gradient: float) -> str:
"""Determine gradient category based on slope thresholds.
Args:
@ -50,7 +52,9 @@ class ElevationRenderer:
else:
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.
Args:
@ -96,7 +100,7 @@ class ElevationRenderer:
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.
Args:
@ -184,7 +188,7 @@ class ElevationRenderer:
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.
Args:
@ -224,7 +228,7 @@ class ElevationRenderer:
'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.
Args:
@ -262,7 +266,7 @@ class ElevationRenderer:
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.
Args:

View File

@ -3,6 +3,8 @@
# author: deng
# date: 20251127
from typing import IO, Optional
import gpxpy
import numpy as np
from geopy.distance import geodesic
@ -11,7 +13,7 @@ from geopy.distance import geodesic
class GPXProcessor:
"""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.
Args:
@ -25,7 +27,7 @@ class GPXProcessor:
self.distances = [] # unit: km
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.
Returns:
@ -37,7 +39,7 @@ class GPXProcessor:
median_sample_time = np.median(diff).total_seconds()
return median_sample_time
def validate_and_parse(self):
def validate_and_parse(self) -> bool:
"""Validate and parse the GPX file.
Returns:
@ -77,7 +79,7 @@ class GPXProcessor:
except Exception as e:
raise Exception(f'GPX 檔案解析失敗: {str(e)}')
def calculate_distance(self):
def calculate_distance(self) -> float:
"""Calculate total distance of the route.
Returns:
@ -96,7 +98,7 @@ class GPXProcessor:
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.
GPS altitude data is notoriously inaccurate. Small fluctuations (noise) can
@ -127,7 +129,7 @@ class GPXProcessor:
return gain, loss
def get_min_max_elevation(self):
def get_min_max_elevation(self) -> tuple[float, float]:
"""Get minimum and maximum elevation.
Returns:
@ -138,7 +140,7 @@ class GPXProcessor:
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.
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])
def get_all_points(self):
def get_all_points(self) -> list[tuple[float, float]]:
"""Get all route points.
Returns:
@ -157,7 +159,7 @@ class GPXProcessor:
"""
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.
Returns:
@ -165,7 +167,7 @@ class GPXProcessor:
"""
return self.distances, self.elevations
def get_gradients(self):
def get_gradients(self) -> list[float]:
"""Calculate gradient (slope) for each segment.
Returns:
@ -188,7 +190,7 @@ class GPXProcessor:
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.
Naismith's Rule:

View File

@ -3,17 +3,26 @@
# author: deng
# date: 20251127
from typing import Optional
import folium
class MapRenderer:
"""Render hiking routes on interactive maps."""
def __init__(self):
def __init__(self) -> None:
"""Initialize map renderer."""
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.
Args:
@ -117,7 +126,7 @@ class MapRenderer:
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.
Args:

View File

@ -9,6 +9,6 @@ import streamlit as st
@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:
return base64.b64encode(f.read()).decode()

View File

@ -3,19 +3,21 @@
# author: deng
# date: 20251127
from typing import Optional
import requests
class WeatherFetcher:
"""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."""
self.api_url = api_url
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: Optional[float]) -> str:
"""Convert wind direction from degrees to flow direction emoji.
Args:
@ -30,7 +32,7 @@ class WeatherFetcher:
index = round(degrees / 45) % 8
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.
Args:
@ -51,7 +53,7 @@ class WeatherFetcher:
else:
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.
Args:
@ -74,7 +76,7 @@ class WeatherFetcher:
else:
return '🟣' # Extreme
def get_weather(self, latitude, longitude):
def get_weather(self, latitude: float, longitude: float) -> Optional[dict]:
"""Fetch weather data for given coordinates.
Args:

View File

@ -1,6 +1,6 @@
[project]
name = "hiking-assistant"
version = "1.0.0"
version = "1.0.1"
description = "This is a web app to analyze gpx for hiking planning"
readme = "README.md"
requires-python = ">=3.13"

2
uv.lock generated
View File

@ -227,7 +227,7 @@ wheels = [
[[package]]
name = "hiking-assistant"
version = "1.0.0"
version = "1.0.1"
source = { virtual = "." }
dependencies = [
{ name = "folium" },