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()
distances, elevations = gpx_processor.get_elevation_profile_data()
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
st.session_state.gpx_file_key = file_key
@ -127,7 +130,7 @@ class HikingAssistant:
st.metric(
label='預估行進時間',
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

View File

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

View File

@ -4,7 +4,7 @@
# date: 20251127
import gpxpy
import gpxpy.gpx
import numpy as np
from geopy.distance import geodesic
@ -19,10 +19,23 @@ class GPXProcessor:
"""
self.gpx_file = gpx_file
self.gpx = None
self.time = []
self.points = []
self.elevations = []
self.elevations = [] # unit: m
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):
"""Validate and parse the GPX file.
@ -43,8 +56,9 @@ class GPXProcessor:
for track in self.gpx.tracks:
for segment in track.segments:
for point in segment.points:
self.points.append((point.latitude, point.longitude))
self.elevations.append(point.elevation if point.elevation else 0)
self.time.append(point.time)
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
@ -70,41 +84,34 @@ class GPXProcessor:
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.
GPS altitude data is notoriously inaccurate. Small fluctuations (noise) can
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:
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.
Returns:
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
gain = 0.0
loss = 0.0
# Use cumulative approach to avoid missing real elevation changes
# Track cumulative change since last significant threshold crossing
cumulative_change = 0.0
for i in range(window_size, len(self.elevations), window_size):
diff = self.elevations[i] - self.elevations[i - window_size]
for i in range(1, len(self.elevations)):
diff = self.elevations[i] - self.elevations[i - 1]
cumulative_change += 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
if diff >= threshold:
gain += diff
elif diff <= -threshold:
loss += abs(diff)
return gain, loss
@ -169,12 +176,12 @@ class GPXProcessor:
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.
Naismith's Rule:
- Base time: 1 hour per 5 km of horizontal distance
- Add time: 1 hour per 600 meters of ascent
- Horizontal speed: 1 hour per 5 km of horizontal distance
- Vertical speed: 1 hour per 600 meters of ascent
Returns:
int: Estimated time in minutes
@ -187,11 +194,8 @@ class GPXProcessor:
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
time_for_distance = total_distance / horizontal_speed
time_for_ascent = elevation_gain / vertical_speed
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_lon = sum(p[1] for p in points) / len(points)
# Create map ojb
# Create map obj
route_map = folium.Map(
location=[center_lat, center_lon],
zoom_start=13,
tiles=None, # Don't add default tiles, we'll add custom ones
tiles=None,
)
# Add OpenStreetMap layer
folium.TileLayer(
tiles='OpenStreetMap',
name='OpenStreetMap',
name='平面(OpenStreetMap',
overlay=False,
control=True,
show=tile_layer == 'OpenStreetMap',
@ -52,7 +52,7 @@ class MapRenderer:
folium.TileLayer(
tiles='https://tile.happyman.idv.tw/map/moi_osm/{z}/{x}/{y}.png',
attr='&copy; <a href="https://rudy.basecamp.tw/">魯地圖</a>',
name='魯地圖 (Rudy Map)',
name='等高線(魯地圖)',
overlay=False,
control=True,
show=tile_layer == 'RudyMap',
@ -80,7 +80,7 @@ class MapRenderer:
fill=True,
fill_color='blue',
fill_opacity=0.7,
popup='起點',
popup=start_point,
tooltip='起點',
).add_to(route_map)
@ -93,7 +93,7 @@ class MapRenderer:
fill=True,
fill_color='blue',
fill_opacity=0.7,
popup='終點',
popup=end_point,
tooltip='終點',
).add_to(route_map)