mod naismith’s rule

This commit is contained in:
deng
2025-11-28 18:50:42 +08:00
parent fd03436d61
commit 37d60d0529
4 changed files with 48 additions and 38 deletions

View File

@ -69,7 +69,10 @@ class HikingAssistant:
all_points = gpx_processor.get_all_points() all_points = gpx_processor.get_all_points()
distances, elevations = gpx_processor.get_elevation_profile_data() distances, elevations = gpx_processor.get_elevation_profile_data()
gradients = gpx_processor.get_gradients() gradients = gpx_processor.get_gradients()
estimated_time = gpx_processor.calculate_naismith_time() estimated_time = gpx_processor.calculate_naismith_time(
horizontal_speed=self._config['app']['estimated_time']['horizontal_speed'],
vertical_speed=self._config['app']['estimated_time']['vertical_speed'],
)
# Cache all processed data # Cache all processed data
st.session_state.gpx_file_key = file_key st.session_state.gpx_file_key = file_key
@ -127,7 +130,7 @@ class HikingAssistant:
st.metric( st.metric(
label='預估行進時間', label='預估行進時間',
value=f'{estimated_time // 60}h {estimated_time % 60}m', value=f'{estimated_time // 60}h {estimated_time % 60}m',
help='依據[Naismiths Rule](https://en.wikipedia.org/wiki/Naismith%27s_rule)計算', help='基於[Naismiths Rule](https://en.wikipedia.org/wiki/Naismith%27s_rule)修正後計算',
) )
# Map section # Map section

View File

@ -6,5 +6,8 @@ app:
elevation_threshold: 2100 elevation_threshold: 2100
warning_text: 此路線海拔較高,請留意[高山症](https://www.ysnp.gov.tw/StaticPage/MountainSickness)發生風險 warning_text: 此路線海拔較高,請留意[高山症](https://www.ysnp.gov.tw/StaticPage/MountainSickness)發生風險
emoji: 💊 emoji: 💊
estimated_time:
horizontal_speed: 3
vertical_speed: 400
weather: weather:
forecast_days: 7 forecast_days: 7

View File

@ -4,7 +4,7 @@
# date: 20251127 # date: 20251127
import gpxpy import gpxpy
import gpxpy.gpx import numpy as np
from geopy.distance import geodesic from geopy.distance import geodesic
@ -19,10 +19,23 @@ class GPXProcessor:
""" """
self.gpx_file = gpx_file self.gpx_file = gpx_file
self.gpx = None self.gpx = None
self.time = []
self.points = [] self.points = []
self.elevations = [] self.elevations = [] # unit: m
self.distances = [] # unit: km self.distances = [] # unit: km
def _get_sample_rate(self):
"""Get the median time interval between points.
Returns:
float: Median sample time interval in seconds
"""
if len(self.time) < 2:
return 0
diff = np.diff(self.time)
median_sample_time = np.median(diff).total_seconds()
return median_sample_time
def validate_and_parse(self): def validate_and_parse(self):
"""Validate and parse the GPX file. """Validate and parse the GPX file.
@ -43,8 +56,9 @@ class GPXProcessor:
for track in self.gpx.tracks: for track in self.gpx.tracks:
for segment in track.segments: for segment in track.segments:
for point in segment.points: for point in segment.points:
self.points.append((point.latitude, point.longitude)) self.time.append(point.time)
self.elevations.append(point.elevation if point.elevation else 0) self.points.append((round(point.latitude, 6), round(point.longitude, 6)))
self.elevations.append(round(point.elevation, 1) if point.elevation else 0)
return len(self.points) > 0 return len(self.points) > 0
@ -70,41 +84,34 @@ class GPXProcessor:
return total_distance return total_distance
def calculate_elevation_gain_loss(self, threshold=3.0): def calculate_elevation_gain_loss(self, threshold=1.0):
"""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
accumulate to create inflated elevation gain/loss values. This method uses accumulate to create inflated elevation gain/loss values. This method uses
a threshold to filter out GPS noise. a threshold to filter out GPS noise and downsampling to reduce computation.
Args: Args:
threshold: Minimum elevation change in meters to be counted (default: 3.0) threshold: Minimum elevation change in meters to be counted (default: 1.0)
This filters out GPS noise while preserving real elevation changes. This filters out GPS noise while preserving real elevation changes.
Returns: Returns:
tuple: (elevation_gain, elevation_loss) in meters tuple: (elevation_gain, elevation_loss) in meters
""" """
if len(self.elevations) < 2: window_size = int(120 / self._get_sample_rate())
if len(self.elevations) <= window_size:
return 0.0, 0.0 return 0.0, 0.0
gain = 0.0 gain = 0.0
loss = 0.0 loss = 0.0
# Use cumulative approach to avoid missing real elevation changes for i in range(window_size, len(self.elevations), window_size):
# Track cumulative change since last significant threshold crossing diff = self.elevations[i] - self.elevations[i - window_size]
cumulative_change = 0.0
for i in range(1, len(self.elevations)): if diff >= threshold:
diff = self.elevations[i] - self.elevations[i - 1] gain += diff
cumulative_change += diff elif diff <= -threshold:
loss += abs(diff)
# Only count if cumulative change exceeds threshold
if cumulative_change >= threshold:
gain += cumulative_change
cumulative_change = 0.0
elif cumulative_change <= -threshold:
loss += abs(cumulative_change)
cumulative_change = 0.0
return gain, loss return gain, loss
@ -169,12 +176,12 @@ class GPXProcessor:
return gradients return gradients
def calculate_naismith_time(self): def calculate_naismith_time(self, horizontal_speed=5, vertical_speed=600):
"""Calculate estimated hiking time using Naismith's Rule. """Calculate estimated hiking time using Naismith's Rule.
Naismith's Rule: Naismith's Rule:
- Base time: 1 hour per 5 km of horizontal distance - Horizontal speed: 1 hour per 5 km of horizontal distance
- Add time: 1 hour per 600 meters of ascent - Vertical speed: 1 hour per 600 meters of ascent
Returns: Returns:
int: Estimated time in minutes int: Estimated time in minutes
@ -187,11 +194,8 @@ class GPXProcessor:
elevation_gain, _ = self.calculate_elevation_gain_loss() elevation_gain, _ = self.calculate_elevation_gain_loss()
# Naismith's Rule calculation # Naismith's Rule calculation
# 1 hour per 5 km = 0.2 hours per km time_for_distance = total_distance / horizontal_speed
time_for_distance = total_distance * 0.2 time_for_ascent = elevation_gain / vertical_speed
# 1 hour per 600 meters of ascent
time_for_ascent = elevation_gain / 600.0
total_time = int((time_for_distance + time_for_ascent) * 60) total_time = int((time_for_distance + time_for_ascent) * 60)

View File

@ -32,17 +32,17 @@ class MapRenderer:
center_lat = sum(p[0] for p in points) / len(points) center_lat = sum(p[0] for p in points) / len(points)
center_lon = sum(p[1] for p in points) / len(points) center_lon = sum(p[1] for p in points) / len(points)
# Create map ojb # Create map obj
route_map = folium.Map( route_map = folium.Map(
location=[center_lat, center_lon], location=[center_lat, center_lon],
zoom_start=13, zoom_start=13,
tiles=None, # Don't add default tiles, we'll add custom ones tiles=None,
) )
# Add OpenStreetMap layer # Add OpenStreetMap layer
folium.TileLayer( folium.TileLayer(
tiles='OpenStreetMap', tiles='OpenStreetMap',
name='OpenStreetMap', name='平面(OpenStreetMap',
overlay=False, overlay=False,
control=True, control=True,
show=tile_layer == 'OpenStreetMap', show=tile_layer == 'OpenStreetMap',
@ -52,7 +52,7 @@ class MapRenderer:
folium.TileLayer( folium.TileLayer(
tiles='https://tile.happyman.idv.tw/map/moi_osm/{z}/{x}/{y}.png', tiles='https://tile.happyman.idv.tw/map/moi_osm/{z}/{x}/{y}.png',
attr='&copy; <a href="https://rudy.basecamp.tw/">魯地圖</a>', attr='&copy; <a href="https://rudy.basecamp.tw/">魯地圖</a>',
name='魯地圖 (Rudy Map)', name='等高線(魯地圖)',
overlay=False, overlay=False,
control=True, control=True,
show=tile_layer == 'RudyMap', show=tile_layer == 'RudyMap',
@ -80,7 +80,7 @@ class MapRenderer:
fill=True, fill=True,
fill_color='blue', fill_color='blue',
fill_opacity=0.7, fill_opacity=0.7,
popup='起點', popup=start_point,
tooltip='起點', tooltip='起點',
).add_to(route_map) ).add_to(route_map)
@ -93,7 +93,7 @@ class MapRenderer:
fill=True, fill=True,
fill_color='blue', fill_color='blue',
fill_opacity=0.7, fill_opacity=0.7,
popup='終點', popup=end_point,
tooltip='終點', tooltip='終點',
).add_to(route_map) ).add_to(route_map)