Updated the banner, added: Elevation and Calories

This commit is contained in:
2026-04-30 13:13:13 +02:00
parent e5ee1d7be5
commit 80c82097c3

View File

@@ -352,7 +352,11 @@ def process_gpx(file_path):
df['time_loc'] = df['time'].dt.tz_localize(None)
df['time_diff'] = df['time'] - df['time'].iloc[0]
df['time_diff_sec'] = df['time_diff'].dt.total_seconds()
df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0])
#df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0])
secs = df['time_diff'].dt.total_seconds().astype(int)
df['duration_hms'] = (secs // 3600).astype(int).astype(str).str.zfill(2) + ':' + \
((secs % 3600) // 60).astype(int).astype(str).str.zfill(2) + ':' + \
(secs % 60).astype(int).astype(str).str.zfill(2)
# Kumulative Distanz (gleiche Logik wie in deiner FIT-Funktion)
distances = [0]
@@ -445,6 +449,70 @@ def safe_add_column_to_dataframe(df, column_name, values):
# =============================================================================
# NEU: Elevation Gain & Calories Hilfsfunktionen
# =============================================================================
# ── Biometrics (kalibriert gegen Strava) ─────────────────────────────────────
_WEIGHT_KG = 75.0
_HEIGHT_CM = 178.0
_AGE_YEARS = 35
_IS_MALE = True
_HR_REST = 64.5 # Ruhepuls in bpm kalibriert gegen Strava 550 kcal
# ─────────────────────────────────────────────────────────────────────────────
def calculate_elevation_gain(df):
if 'elev' not in df.columns or df['elev'].isna().all():
return 0
elev_smooth = df['elev'].rolling(
window=30, win_type='gaussian', center=True, min_periods=1
).mean(std=5)
delta = elev_smooth.diff().fillna(0)
# Schwellwert relativ zur Gesamtamplitude statt absolut fest
amplitude = df['elev'].max() - df['elev'].min()
threshold = amplitude * 0.00095 # ~0.4% der Gesamtamplitude
gain = delta[delta > threshold].sum()
print(f"DEBUG elev: min={df['elev'].min():.1f}, max={df['elev'].max():.1f}, delta>0.03={delta[delta>0.03].sum():.1f}")
return int(round(gain))
def calculate_calories_burned(df):
# Geschlechts- und altersbasierte HR-Zonen nach Karvonen
# Kein fixer HR_REST — nutze den tatsächlichen Minimum-HR aus dem Lauf
# als Annäherung an den Ruhepuls
hr_max = 220 - _AGE_YEARS
use_hr = ('heart_rate' in df.columns and
df['heart_rate'].notna().sum() > len(df) * 0.5)
if not use_hr:
return 0
hr = df['heart_rate'].ffill().fillna(df['heart_rate'].bfill()).values
ts = df['time_diff_sec'].values
# Ruhepuls aus dem Lauf selbst schätzen: 5. Perzentile der HR-Werte
hr_rest = float(df['heart_rate'].quantile(0.05))
hr_rest = max(45.0, min(hr_rest, 80.0)) # Plausibilitätsgrenze
vo2max = 15.0 * (hr_max / hr_rest)
cumulative = 0.0
for i in range(1, len(ts)):
dt = ts[i] - ts[i - 1]
if dt <= 0 or dt > 10:
continue
frac = max(0.0, (hr[i] - hr_rest) / (hr_max - hr_rest))
met = max(1.0, min((frac * vo2max) / 3.5, 18.0))
kcal_per_s = met * _WEIGHT_KG * 3.5 / 12000.0
cumulative += kcal_per_s * dt
print(f"DEBUG calories: rows={len(df)}, hr_valid={df['heart_rate'].notna().sum()}, duration={df['time_diff_sec'].iloc[-1]:.0f}s, hr_mean={df['heart_rate'].mean():.1f}")
return int(round(cumulative))
# =============================================================================
# INFO BANNER
@@ -468,22 +536,38 @@ def create_info_banner(df):
else:
formatted_pace = "N/A"
# Elevation Gain
elevation_gain_m = calculate_elevation_gain(df)
# Calories
total_calories = calculate_calories_burned(df)
# Build the info banner layout
info_banner = html.Div([
html.Div([
html.H4("Total Distance", style={'margin-bottom': '5px'}),
html.H2(f"{total_distance_km:.2f} km")
], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),
], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}),
html.Div([
html.H4("Total Time", style={'margin-bottom': '5px'}),
html.H2(formatted_total_time)
], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),
], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}),
html.Div([
html.H4("Average Pace", style={'margin-bottom': '5px'}),
html.H2(formatted_pace)
], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),
], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}),
html.Div([
html.H4("Elevation", style={'margin-bottom': '5px'}),
html.H2(f"{elevation_gain_m} m")
], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}),
html.Div([
html.H4("Calories", style={'margin-bottom': '5px'}),
html.H2(f"{total_calories} kcal")
], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}),
], style={
'display': 'flex',
'justifyContent': 'space-around',
@@ -1395,7 +1479,9 @@ app.layout = html.Div([
Input('file-dropdown', 'value')
)
def load_data(selected_file): # Dateipfad der ausgewählten Datei
print(f"DEBUG load_data: {selected_file}")
df = process_selected_file(selected_file) # Verarbeitet diese Datei
print(f"DEBUG load_data: rows={len(df)}")
return df.to_json(date_format='iso', orient='split')
# Callback 2: Update All (static) Plots