#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Jul 30th 2025

@author: Marcel Weschke
@email: marcel.weschke@directbox.de
"""
# %% Load libraries
import os
import base64
import io
import datetime
from math import radians, sin, cos, sqrt, asin

import dash
from dash import dcc, html, Input, Output, Dash, State
import dash_bootstrap_components as dbc

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from scipy.interpolate import interp1d
import gpxpy
from fitparse import FitFile

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.colors import LinearSegmentedColormap, Normalize

from xml.etree.ElementTree import Element, SubElement, tostring
import xml.etree.ElementTree as ET

# =============================================================================
# Helper Functions
# =============================================================================
def list_files():
    """
    Listet alle .fit Files aus fit_files/ und .gpx Files aus gpx_files/ auf
    und sortiert sie nach Datum (neueste zuerst)
    """

    # Definiere Ordner und Dateierweiterungen
    folders_config = [
        {'folder': './fit_files', 'extensions': ['.fit'], 'type': 'FIT'},
        {'folder': './gpx_files', 'extensions': ['.gpx'], 'type': 'GPX'}
    ]

    all_file_options = []

    for config in folders_config:
        folder = config['folder']
        extensions = config['extensions']
        file_type = config['type']

        # Prüfe ob Ordner existiert
        if not os.path.exists(folder):
            print(f"Ordner {folder} existiert nicht!")
            continue

        # Hole alle Files mit den entsprechenden Erweiterungen
        try:
            all_files = os.listdir(folder)
            files = [f for f in all_files
                    if any(f.lower().endswith(ext) for ext in extensions)]
        except Exception as e:
            print(f"Fehler beim Lesen des Ordners {folder}: {e}")
            continue

        # Erstelle Optionen für diesen Ordner
        for f in files:
            file_path = os.path.join(folder, f)

            # Extrahiere Datum für Sortierung
            file_date = extract_date_from_file(f, file_path)

            # Erstelle Label mit Dateityp-Info
            try:
                size_mb = os.path.getsize(file_path) / (1024 * 1024)
                mod_time = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
                #label = f"[{file_type}] {f}"
                label = f"{f}"
                # Optional: Erweiterte Info (auskommentiert für sauberere Ansicht)
                # label = f"[{file_type}] {f} ({size_mb:.1f}MB - {mod_time.strftime('%d.%m.%Y %H:%M')})"
            except:
                #label = f"[{file_type}] {f}"
                label = f"{f}"

            all_file_options.append({
                'label': label,
                'value': file_path,
                'date': file_date,
                'type': file_type
            })

    # Sortiere alle Files nach Datum (neueste zuerst)
    all_file_options.sort(key=lambda x: x['date'], reverse=True)

    # Entferne 'date' und 'type' aus den finalen Optionen (nur für Sortierung gebraucht)
    final_options = [{'label': opt['label'], 'value': opt['value']}
                    for opt in all_file_options]

    # Fallback wenn keine Files gefunden
    if not final_options:
        return [{'label': 'Keine .fit oder .gpx Dateien gefunden', 'value': 'NO_FILES'}]

    return final_options

def extract_date_from_file(filename, file_path):
    """Extrahiert Datum aus Filename für Sortierung"""
    try:
        # Versuche verschiedene Datumsformate im Dateinamen
        # Format: dd.mm.yyyy
        return datetime.datetime.strptime(filename[:10], '%d.%m.%Y')
    except ValueError:
        try:
            # Format: yyyy-mm-dd
            return datetime.datetime.strptime(filename[:10], '%Y-%m-%d')
        except ValueError:
            try:
                # Format: yyyymmdd
                return datetime.datetime.strptime(filename[:8], '%Y%m%d')
            except ValueError:
                try:
                    # Format: yyyy_mm_dd
                    return datetime.datetime.strptime(filename[:10], '%Y_%m_%d')
                except ValueError:
                    try:
                        # Format: dd-mm-yyyy
                        return datetime.datetime.strptime(filename[:10], '%d-%m-%Y')
                    except ValueError:
                        # Wenn kein Datum erkennbar, nutze Datei-Änderungsdatum
                        try:
                            return datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
                        except:
                            return datetime.datetime.min


def haversine(lon1, lat1, lon2, lat2):
    """
    Berechnet die Entfernung zwischen zwei GPS-Koordinaten in km
    """
    R = 6371  # Erdradius in km
    dlon = radians(lon2 - lon1)
    dlat = radians(lat2 - lat1)
    a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
    return 2 * R * asin(sqrt(a))


# -----------------------------------------------------------------------------
# FIT-FILE-FUNCTION
# -----------------------------------------------------------------------------
def process_fit(file_path):
    """
    Verarbeitet eine FIT-Datei und erstellt einen DataFrame
    """
    if file_path in ['NO_FILE', 'NO_FOLDER', 'ERROR']:
        print(f"Ungültiger Dateipfad: {file_path}")
        return pd.DataFrame()

    if not os.path.exists(file_path):
        print(f"Datei nicht gefunden: {file_path}")
        return pd.DataFrame()

    try:
        fit_file = FitFile(file_path)
        print(f"\nVerarbeite FIT-Datei: {file_path}")

        # Sammle alle record-Daten
        records = []
        for record in fit_file.get_messages("record"):
            record_data = {}
            for data in record:
                # Sammle alle verfügbaren Datenfelder
                record_data[data.name] = data.value
            records.append(record_data)

        if not records:
            print("Keine Aufzeichnungsdaten in der FIT-Datei gefunden")
            return pd.DataFrame()

        # Erstelle DataFrame
        df = pd.DataFrame(records)
        print(f"DataFrame erstellt mit {len(df)} Zeilen und Spalten: {list(df.columns)}")

        # Debugging: Schaue welche Spalten verfügbar sind
        # print(f"Verfügbare Spalten: {df.columns.tolist()}")     # Uncomment if needed - DEBUG purpose!

        # Suche nach Heart Rate in verschiedenen Formaten
        possible_hr_cols = [col for col in df.columns if 'heart' in col.lower() or 'hr' in col.lower()]
        # print(f"Mögliche Heart Rate Spalten: {possible_hr_cols}")     # Uncomment if needed - DEBUG purpose!

        # Standard-Spaltennamen für verschiedene FIT-Formate
        lat_cols = ['position_lat', 'lat', 'latitude']
        lon_cols = ['position_long', 'lon', 'longitude']
        elev_cols = ['altitude', 'elev', 'elevation', 'enhanced_altitude']
        time_cols = ['timestamp', 'time']
        hr_cols = ['heart_rate', 'hr'] + possible_hr_cols
        speed_cols = ['speed', 'enhanced_speed']
        dist_cols = ['distance', 'total_distance']

        # Finde die richtigen Spaltennamen
        lat_col = next((col for col in lat_cols if col in df.columns), None)
        lon_col = next((col for col in lon_cols if col in df.columns), None)
        elev_col = next((col for col in elev_cols if col in df.columns), None)
        time_col = next((col for col in time_cols if col in df.columns), None)
        hr_col = next((col for col in hr_cols if col in df.columns), None)
        speed_col = next((col for col in speed_cols if col in df.columns), None)

        # Prüfe ob wichtige Daten vorhanden sind
        if not lat_col or not lon_col or not time_col:
            raise ValueError(f"Wichtige Daten fehlen! Lat: {lat_col}, Lon: {lon_col}, Time: {time_col}")

        # Benenne Spalten einheitlich um
        df = df.rename(columns={
            lat_col: 'lat',
            lon_col: 'lon',
            elev_col: 'elev' if elev_col else None,
            time_col: 'time',
            hr_col: 'heart_rate' if hr_col else None,
            speed_col: 'speed_ms' if speed_col else None
        })

        # FIT lat/lon sind oft in semicircles - konvertiere zu Grad
        if df['lat'].max() > 180:  # Semicircles detection
            df['lat'] = df['lat'] * (180 / 2**31)
            df['lon'] = df['lon'] * (180 / 2**31)

        # Entferne Zeilen ohne GPS-Daten
        df = df.dropna(subset=['lat', 'lon', 'time']).reset_index(drop=True)

        # Basic cleanup
        df['time'] = pd.to_datetime(df['time'])
        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])

        # Cumulative distance (km) (vektorisiert)
        lat_r = np.radians(df['lat'].values)
        lon_r = np.radians(df['lon'].values)
        dlat  = np.diff(lat_r, prepend=lat_r[0])
        dlon  = np.diff(lon_r, prepend=lon_r[0])
        a     = np.sin(dlat/2)**2 + np.cos(lat_r) * np.cos(np.roll(lat_r,1)) * np.sin(dlon/2)**2
        a[0]  = 0
        step_dist = 2 * 6371 * np.arcsin(np.sqrt(np.clip(a, 0, 1)))
        df['cum_dist_km'] = np.cumsum(step_dist)

        # Elevation handling
        if 'elev' in df.columns:
            df['elev'] = df['elev'].bfill()
            df['delta_elev'] = df['elev'].diff().fillna(0)
            df['rel_elev'] = df['elev'] - df['elev'].iloc[0]
        else:
            # Fallback wenn keine Elevation vorhanden
            df['elev'] = 0
            df['delta_elev'] = 0
            df['rel_elev'] = 0

        # Speed calculation
        if 'speed_ms' in df.columns:
            # Konvertiere m/s zu km/h
            df['speed_kmh'] = df['speed_ms'] * 3.6
        else:
            # Fallback: Berechne Speed aus GPS-Daten
            df['delta_t'] = df['time'].diff().dt.total_seconds()
            df['delta_d'] = df['cum_dist_km'].diff()
            df['speed_kmh'] = (df['delta_d'] / df['delta_t']) * 3600
            df['speed_kmh'] = df['speed_kmh'].replace([np.inf, -np.inf], np.nan)

        # Velocity (used in pace calculations)
        df['vel_kmps'] = np.gradient(df['cum_dist_km'], df['time_diff_sec'])

        # Smoothed speed (Gaussian rolling)
        df['speed_kmh_smooth'] = df['speed_kmh'].rolling(window=10, win_type="gaussian", center=True).mean(std=2)


        # Heart rate handling
        # Zweiter Durchlauf nötig: nach dropna() hat df weniger Zeilen als records,
        # safe_add_column_to_dataframe behandelt den Längenunterschied korrekt.
        heart_rate = []
        for record in fit_file.get_messages("record"):
            for data in record:
                if data.name == 'heart_rate':
                    heart_rate.append(data.value)
        df = safe_add_column_to_dataframe(df, 'heart_rate', heart_rate)

        if 'heart_rate' in df.columns:
            df['heart_rate'] = pd.to_numeric(df['heart_rate'], errors='coerce')
            #df['hr_smooth']  = df['heart_rate'].rolling(window=5, center=True).mean()
            df['hr_smooth'] = df['heart_rate'].rolling(window=2, center=True, min_periods=1).mean()
        else:
            df['heart_rate'] = np.nan
            df['hr_smooth']  = np.nan

            print(f"Verarbeitete FIT-Datei: {len(df)} Datenpunkte")
            print(f"Distanz: {df['cum_dist_km'].iloc[-1]:.2f} km")
            print(f"Dauer: {df['duration_hms'].iloc[-1]}")

        return df

    except Exception as e:
        print(f"Fehler beim Verarbeiten der FIT-Datei {file_path}: {str(e)}")
        return pd.DataFrame()



# -----------------------------------------------------------------------------
# GPX-FILE-FUNCTION
# -----------------------------------------------------------------------------
def process_gpx(file_path):
    """
    Verarbeitet GPX-Dateien und gibt DataFrame im gleichen Format wie process_fit() zurück
    """
    import gpxpy
    import gpxpy.gpx

    try:
        with open(file_path, 'r', encoding='utf-8') as gpx_file:
            gpx = gpxpy.parse(gpx_file)

        print(f"Verarbeite GPX-Datei: {file_path}")

        # Sammle GPS-Punkte aus allen Tracks/Segments
        points_data = []
        for track in gpx.tracks:
            for segment in track.segments:
                for point in segment.points:
                    points_data.append({
                        'time': point.time,
                        'lat': point.latitude,
                        'lon': point.longitude,
                        'elev': point.elevation if point.elevation else 0,
                        'heart_rate': None  # GPX hat normalerweise keine HR-Daten
                    })

        if not points_data:
            print("Keine GPS-Daten in GPX-Datei gefunden")
            return pd.DataFrame()

        # Erstelle DataFrame
        df = pd.DataFrame(points_data)
        print(f"GPX DataFrame erstellt mit {len(df)} Zeilen")

        # Sortiere nach Zeit
        df = df.sort_values('time').reset_index(drop=True)

        # Zeit-Verarbeitung (wie in deiner FIT-Funktion)
        df['time'] = pd.to_datetime(df['time'])
        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])
        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)

        # Cumulative distance (km) (vektorisiert)
        lat_r = np.radians(df['lat'].values)
        lon_r = np.radians(df['lon'].values)
        dlat  = np.diff(lat_r, prepend=lat_r[0])
        dlon  = np.diff(lon_r, prepend=lon_r[0])
        a     = np.sin(dlat/2)**2 + np.cos(lat_r) * np.cos(np.roll(lat_r,1)) * np.sin(dlon/2)**2
        a[0]  = 0
        step_dist = 2 * 6371 * np.arcsin(np.sqrt(np.clip(a, 0, 1)))
        df['cum_dist_km'] = np.cumsum(step_dist)

        # Elevation (gleiche Logik wie in deiner FIT-Funktion)
        df['elev'] = df['elev'].bfill()
        df['delta_elev'] = df['elev'].diff().fillna(0)
        df['rel_elev'] = df['elev'] - df['elev'].iloc[0]

        # Speed-Berechnung (gleiche Logik wie dein Fallback)
        df['delta_t'] = df['time'].diff().dt.total_seconds()
        df['delta_d'] = df['cum_dist_km'].diff()
        df['speed_kmh'] = (df['delta_d'] / df['delta_t']) * 3600
        df['speed_kmh'] = df['speed_kmh'].replace([np.inf, -np.inf], np.nan)

        # Velocity (wie in deiner FIT-Funktion)
        df['vel_kmps'] = np.gradient(df['cum_dist_km'], df['time_diff_sec'])

        # Smoothed speed (wie in deiner FIT-Funktion)
        df['speed_kmh_smooth'] = df['speed_kmh'].rolling(window=10, win_type="gaussian", center=True).mean(std=2)

        # Heart rate (GPX hat keine, also NaN wie dein Fallback)
        df['heart_rate'] = np.nan
        df['hr_smooth'] = np.nan

        print(f"Verarbeitete GPX-Datei: {len(df)} Datenpunkte")
        print(f"Distanz: {df['cum_dist_km'].iloc[-1]:.2f} km")
        print(f"Dauer: {df['duration_hms'].iloc[-1]}")

        return df

    except Exception as e:
        print(f"Fehler beim Verarbeiten der GPX-Datei {file_path}: {str(e)}")
        return pd.DataFrame()

# NEUE UNIVERSELLE WRAPPER-FUNKTION (nutzt deine bestehenden Funktionen!)
def process_selected_file(file_path):
    """
    Universelle Funktion die automatisch FIT oder GPX verarbeitet
    """
    if not file_path or file_path in ['NO_FILES', 'NO_FOLDER', 'ERROR']:
        return pd.DataFrame()

    # Bestimme Dateityp
    if file_path.lower().endswith('.fit'):
        # NUTZT DEINE ORIGINALE FUNKTION!
        return process_fit(file_path)
    elif file_path.lower().endswith('.gpx'):
        # Nutzt die neue GPX-Funktion
        return process_gpx(file_path)
    else:
        print(f"Unbekannter Dateityp: {file_path}")
        return pd.DataFrame()




def safe_add_column_to_dataframe(df, column_name, values):
    """
    Fügt eine Spalte sicher zu einem DataFrame hinzu, auch wenn die Längen nicht übereinstimmen
    """
    if df.empty:
        return df

    df_len = len(df)
    values_len = len(values) if hasattr(values, '__len__') else 0

    if values_len == df_len:
        # Perfekt - gleiche Länge
        df[column_name] = values
    elif values_len > df_len:
        # Zu viele Werte - kürze sie
        print(f"WARNUNG: {column_name} hat {values_len} Werte, DataFrame hat {df_len} Zeilen. Kürze Werte.")
        df[column_name] = values[:df_len]
    elif values_len < df_len:
        # Zu wenige Werte - fülle mit NaN auf
        print(f"WARNUNG: {column_name} hat {values_len} Werte, DataFrame hat {df_len} Zeilen. Fülle mit NaN auf.")
        extended_values = list(values) + [None] * (df_len - values_len)
        df[column_name] = extended_values
    else:
        # Keine Werte - fülle mit NaN
        print(f"WARNUNG: Keine Werte für {column_name}. Fülle mit NaN.")
        df[column_name] = [None] * df_len

    return df



# =============================================================================
# NEU: Elevation Gain & Calories – Hilfsfunktionen
# =============================================================================

# ── Biometrics (kalibriert gegen Strava) ─────────────────────────────────────
_WEIGHT_KG  = 75.0
_HEIGHT_CM  = 178.0
_AGE_YEARS  = 36
_IS_MALE    = True
_HR_REST    = 64.5   # Ruhepuls in bpm – kalibriert gegen Strava 550 kcal
HR_MAX_BPM = 220 - _AGE_YEARS   # z.B. 185 bei Alter 35
HR_ZONES = [
    {"name": "Zone 1", "lower":   0, "upper": 124, "color": "#7FB3FF", "label": "Recovery"},
    {"name": "Zone 2", "lower": 124, "upper": 154, "color": "#9DE79A", "label": "Ausdauer"},
    {"name": "Zone 3", "lower": 154, "upper": 169, "color": "#FFF29A", "label": "Aerob"},
    {"name": "Zone 4", "lower": 169, "upper": 184, "color": "#FFBE7A", "label": "Schwelle"},
    {"name": "Zone 5", "lower": 184, "upper": 999, "color": "#FF9AA2", "label": "Neuromuск."},
]

def get_zone_for_bpm(bpm):
    """Gibt den Zonen-Index (0-4) für einen BPM-Wert zurück."""
    for i, z in enumerate(HR_ZONES):
        if z["lower"] <= bpm < z["upper"]:
            return i
    return 4



# ─────────────────────────────────────────────────────────────────────────────

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
    hr_rest = _HR_REST  # 64.5 bpm

    # Check, ob genug Herzfrequenz-Daten vorhanden sind
    use_hr = ('heart_rate' in df.columns and
              df['heart_rate'].notna().sum() > len(df) * 0.5)

    if not use_hr:
        return 0

    # Datenvorbereitung
    hr = df['heart_rate'].ffill().fillna(df['heart_rate'].bfill()).values
    ts = df['time_diff_sec'].values

    # 2. VO2max Schätzung basierend auf deinem echten Ruhepuls
    # Die Formel 15 * (HR_max / HR_rest) ist ein Standard-Approximationswert
    vo2max = 15.0 * (hr_max / hr_rest)

    # Zeitdifferenzen berechnen
    dt = np.diff(ts, prepend=ts[0])
    mask = (dt > 0) & (dt <= 10)  # Filtert Pausen oder Sprünge in der Aufzeichnung

    # 3. Berechnung der Intensität (Karvonen-Prinzip)
    # Anteil der Herzfrequenzreserve (HRR)
    frac = np.clip((hr - hr_rest) / (hr_max - hr_rest), 0, None)

    # Umrechnung in MET (Metabolic Equivalent of Task)
    # Ein MET = 3.5 ml VO2/kg/min
    met = np.clip((frac * vo2max) / 3.5, 1.0, 18.0)

    # 4. Umrechnung in kcal pro Sekunde
    # Formel: (MET * 3.5 * Gewicht_kg) / (200 * 60) -> 12000
    kcal_per_s = met * _WEIGHT_KG * 3.5 / 12000.0

    # Summation über den gesamten Lauf
    cumulative = float(np.sum(kcal_per_s[mask] * dt[mask]))

    print(f"DEBUG calories: HR_REST_USED={hr_rest}, VO2MAX_EST={vo2max:.1f}, "
          f"DURATION={df['time_diff_sec'].iloc[-1]/60:.1f}min, TOTAL_KCAL={cumulative:.1f}")

    return int(round(cumulative))


# -----------------------------------------------------------------------------
# INFO BANNER
# -----------------------------------------------------------------------------
def create_info_banner(df):
    # Total distance in km
    total_distance_km = df['cum_dist_km'].iloc[-1]

    # Total time as timedelta
    total_seconds = df['time_diff_sec'].iloc[-1]
    hours, remainder = divmod(int(total_seconds), 3600)
    minutes, seconds = divmod(remainder, 60)
    formatted_total_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}"

    # Average pace (min/km)
    if total_distance_km > 0:
        pace_sec_per_km = total_seconds / total_distance_km
        pace_min = int(pace_sec_per_km // 60)
        pace_sec = int(pace_sec_per_km % 60)
        formatted_pace = f"{pace_min}:{pace_sec:02d} min/km"
    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': '18%', 'display': 'inline-block', 'textAlign': 'center'}),

        html.Div([
            html.H4("Total Time", style={'margin-bottom': '5px'}),
            html.H2(formatted_total_time)
        ], style={'width': '18%', 'display': 'inline-block', 'textAlign': 'center'}),

        html.Div([
            html.H4("Average Pace", style={'margin-bottom': '5px'}),
            html.H2(formatted_pace)
        ], 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',
        'backgroundColor': '#1e1e1e',
        'color': 'white',
        'padding': '5px',
        'marginBottom': '5px',
        'borderRadius': '10px',
        'width': '100%',
        #'maxWidth': '1200px',
        'margin': 'auto'
    })

    return info_banner
  

# -----------------------------------------------------------------------------
# EXPORT SUMMARY IMAGE (SVG)
# -----------------------------------------------------------------------------
def create_strava_style_svg(df, total_distance_km=None, total_time=None, avg_pace=None,
                           width=800, height=600, padding=50):
    """
    Erstellt ein STRAVA-style SVG mit transparentem Hintergrund
    """

    # SVG Root Element
    svg = Element('svg')
    svg.set('width', str(width))
    svg.set('height', str(height))
    svg.set('xmlns', 'http://www.w3.org/2000/svg')
    svg.set('style', 'background: transparent;')

    # Route-Bereich (links 60% der Breite)
    route_width = width * 0.6
    route_height = height - 2 * padding

    # Koordinaten normalisieren für den Route-Bereich
    lats = df['lat'].values
    lons = df['lon'].values

    # Bounding Box der Route
    lat_min, lat_max = lats.min(), lats.max()
    lon_min, lon_max = lons.min(), lons.max()

    # Aspect Ratio beibehalten
    lat_range = lat_max - lat_min
    lon_range = lon_max - lon_min

    if lat_range == 0 or lon_range == 0:
        raise ValueError("Route hat keine Variation in Koordinaten")

    # Skalierung berechnen
    scale_x = (route_width - 2 * padding) / lon_range
    scale_y = (route_height - 2 * padding) / lat_range

    # Einheitliche Skalierung für korrekte Proportionen
    scale = min(scale_x, scale_y)

    # Zentrieren
    center_x = route_width / 2
    center_y = height / 2

    # Route-Pfad erstellen
    path_data = []
    for i, (lat, lon) in enumerate(zip(lats, lons)):
        # Koordinaten transformieren (Y-Achse umkehren für SVG)
        x = center_x + (lon - (lon_min + lon_max) / 2) * scale
        y = center_y - (lat - (lat_min + lat_max) / 2) * scale

        if i == 0:
            path_data.append(f"M {x:.2f} {y:.2f}")
        else:
            path_data.append(f"L {x:.2f} {y:.2f}")

    # Route-Pfad zum SVG hinzufügen
    route_path = SubElement(svg, 'path')
    route_path.set('d', ' '.join(path_data))
    route_path.set('stroke', '#ff6909')  # Deine Routenfarbe
    route_path.set('stroke-width', '4')
    route_path.set('fill', 'none')
    route_path.set('stroke-linecap', 'round')
    route_path.set('stroke-linejoin', 'round')

    # Start-Punkt (grün)
    start_x = center_x + (lons[0] - (lon_min + lon_max) / 2) * scale
    start_y = center_y - (lats[0] - (lat_min + lat_max) / 2) * scale
    start_circle = SubElement(svg, 'circle')
    start_circle.set('cx', str(start_x))
    start_circle.set('cy', str(start_y))
    start_circle.set('r', '8')
    start_circle.set('fill', '#4CAF50')  # Grün
    start_circle.set('stroke', 'white')
    start_circle.set('stroke-width', '2')

    # End-Punkt (rot)
    end_x = center_x + (lons[-1] - (lon_min + lon_max) / 2) * scale
    end_y = center_y - (lats[-1] - (lat_min + lat_max) / 2) * scale
    end_circle = SubElement(svg, 'circle')
    end_circle.set('cx', str(end_x))
    end_circle.set('cy', str(end_y))
    end_circle.set('r', '8')
    end_circle.set('fill', '#f44336')  # Rot
    end_circle.set('stroke', 'white')
    end_circle.set('stroke-width', '2')

    # Stats-Bereich (rechts 40% der Breite)
    stats_x = route_width + padding
    stats_y_start = padding + 50

    ## Hintergrund für Stats (optional, semi-transparent - SCHWARZE BOX)
    #stats_bg = SubElement(svg, 'rect')
    #stats_bg.set('x', str(stats_x - 20))
    #stats_bg.set('y', str(stats_y_start - 30))
    #stats_bg.set('width', str(width * 0.35))
    #stats_bg.set('height', str(250))
    #stats_bg.set('fill', 'rgba(0,0,0,0.7)')
    #stats_bg.set('rx', '10')

    # Stats-Text hinzufügen
    stats = [
        ("TOTAL DISTANCE", f"{total_distance_km:.1f} km" if total_distance_km else "N/A"),
        ("TOTAL TIME", total_time or "N/A"),
        ("AVERAGE PACE", avg_pace or "N/A")
    ]

    for i, (label, value) in enumerate(stats):
        y_pos = stats_y_start + i * 70

        # Label (kleinere Schrift, grau)
        label_text = SubElement(svg, 'text')
        label_text.set('x', str(stats_x))
        label_text.set('y', str(y_pos))
        label_text.set('font-family', 'Arial, sans-serif')
        label_text.set('font-size', '14')
        label_text.set('font-weight', 'bold')
        label_text.set('fill', '#000000')       # TEXTFARBE  #333333
        label_text.text = label

        # Wert (größere Schrift, weiß)
        value_text = SubElement(svg, 'text')
        value_text.set('x', str(stats_x))
        value_text.set('y', str(y_pos + 25))
        value_text.set('font-family', 'Arial, sans-serif')
        value_text.set('font-size', '24')
        value_text.set('font-weight', 'bold')
        value_text.set('fill', 'white')          # TEXTFARBE
        value_text.text = value

    return svg

def save_svg(svg_element, filename="run_overlay.svg"):
    """SVG als Datei speichern"""
    rough_string = tostring(svg_element, 'unicode')

    # Formatierung verbessern
    dom = ET.fromstring(rough_string)
    ET.indent(dom, space="  ", level=0)

    with open(filename, 'w') as f:
        f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
        f.write(ET.tostring(dom, encoding='unicode'))

    print(f"SVG saved as {filename}")

def calculate_pace(distance_km, total_seconds):
    """
    Berechnet das Durchschnittstempo in min/km Format
    """
    if distance_km == 0:
        return "0:00 /km"

    pace_seconds_per_km = total_seconds / distance_km
    pace_minutes = int(pace_seconds_per_km // 60)
    pace_seconds = int(pace_seconds_per_km % 60)

    return f"{pace_minutes}:{pace_seconds:02d} /km"



# -----------------------------------------------------------------------------
# START OF THE PLOTS
# MAP-PLOT:
# -----------------------------------------------------------------------------
def create_map_plot(df):
    fig = px.line_map(
    df,
    lat='lat',
    lon='lon',
    zoom=13.5,
    height=800
    )
    
    # Info: Frankfurt liegt ca. 112 m ü.NN, Hamburg ca. 6 m ü.NN.
    fig.update_traces(
        hovertemplate=(
            "Time:       %{customdata[5]}<br>" +
            "Distance:   %{customdata[0]:.2f} km<br>" +
            "Elevation: %{customdata[1]:.0f} m ASL (Δ %{customdata[2]:+.0f} m from start)<br>" +  #"ASL" = above sea level; diff. vs start value"
            "Speed:      %{customdata[3]:.1f} km/h<br>" +
            "Heart Rate: %{customdata[4]:.0f} bpm<extra></extra>"
        ),
        #customdata=df[['time', 'cum_dist_km', 'duration_hms']]
        #customdata=df[['cum_dist_km', 'speed_kmh', 'heart_rate', 'duration_hms']]
        customdata=df[['cum_dist_km', 'elev', 'rel_elev', 'speed_kmh', 'heart_rate', 'duration_hms']]
    )
    # Define map style and the line ontop
    fig.update_layout(map_style="open-street-map")  #My-Fav: open-street-map, satellite-streets, dark, white-bg
    # Possible Options:
    # 'basic', 'carto-darkmatter', 'carto-darkmatter-nolabels', 'carto-positron', 'carto-positron-nolabels', 'carto-voyager', 'carto-voyager-nolabels', 'dark', 'light', 'open-street-map', 'outdoors', 'satellite', 'satellite-streets', 'streets', 'white-bg'.
    fig.update_traces(line=dict(color="#f54269", width=3))

    # Start / Stop marker
    start = df.iloc[0]
    end = df.iloc[-1]
    fig.add_trace(go.Scattermap(
        lat=[start['lat']], lon=[start['lon']], mode='markers+text',
        marker=dict(size=12, color='#b9fc62', symbol='circle'), text=['Start'], name='Start', textposition='bottom left'  # Starting point !
    ))
    fig.add_trace(go.Scattermap(
        lat=[end['lat']], lon=[end['lon']], mode='markers+text',
        marker=dict(size=12, color='#fca062', symbol='circle'), text=['Stop'], name='Stop', textposition='bottom left'  # Finishing point !
    ))

    # THIS IS MY ELEVATION-PLOT SHOW POSITION-MARKER IN MAP-PLOT:
    fig.add_trace(go.Scattermap(
        lat=[],
        lon=[],
        mode="markers",
        marker=dict(size=18, color="#42B1E5", symbol="circle"),
        name="Hovered Point"
    ))
    # KOMPAKTE LAYOUT-EINSTELLUNGEN
    fig.update_layout(
        paper_bgcolor='#1e1e1e',
        font=dict(color='white'),
        # Margins reduzieren für kompakteren Plot
        margin=dict(l=60, r=45, t=10, b=50),  # Links, Rechts, Oben, Unten
        # Plotly-Toolbar konfigurieren
        showlegend=True,
        # Kompakte Legend
        legend=dict(
            orientation='h',         # horizontal layout
            yanchor='top',
            y=-0.02,                 # move legend below the map
            xanchor='center',
            x=0.5,
            font=dict(color='white', size=10)  # Kleinere Schrift
        )
    )

    return fig






# =============================================================================
# HILFSFUNKTION: GPS → Pixel-Koordinaten (Mercator-Projektion)
# =============================================================================
def _gps_to_pixel(lats, lons, img_width=800, img_height=600,
                  padding=None,
                  pad_top=40, pad_bottom=60, pad_left=10, pad_right=10):
    """
    Konvertiert GPS-Koordinaten (lat/lon) in Pixel-Koordinaten.
    Unterstützt symmetrisches padding (alter Aufruf bleibt kompatibel)
    oder asymmetrisches padding pro Seite.

    Aufruf alt (kompatibel): _gps_to_pixel(lats, lons, padding=50)
    Aufruf neu:              _gps_to_pixel(lats, lons, pad_top=40, pad_bottom=60, ...)
    """
    lats = np.array(lats, dtype=float)
    lons = np.array(lons, dtype=float)

    # Symmetrisches padding überschreibt alle vier Seiten (Rückwärtskompatibilität)
    if padding is not None:
        pad_top = pad_bottom = pad_left = pad_right = padding

    lat_min, lat_max = lats.min(), lats.max()
    lon_min, lon_max = lons.min(), lons.max()
    lat_range = lat_max - lat_min
    lon_range = lon_max - lon_min

    draw_w = img_width  - pad_left - pad_right
    draw_h = img_height - pad_top  - pad_bottom

    if lon_range == 0 or lat_range == 0:
        xs = np.full(len(lats), img_width  / 2)
        ys = np.full(len(lats), img_height / 2)
    else:
        scale = min(draw_w / lon_range, draw_h / lat_range)

        offset_x = pad_left + (draw_w - lon_range * scale) / 2
        offset_y = pad_top  + (draw_h - lat_range * scale) / 2

        xs = offset_x + (lons - lon_min) * scale
        ys = offset_y + (lat_max - lats) * scale   # Y-Achse umkehren

    meta = {
        'lat_min': lat_min, 'lat_max': lat_max,
        'lon_min': lon_min, 'lon_max': lon_max,
        'img_width': img_width, 'img_height': img_height
    }
    return xs, ys, meta


# =============================================================================
# HILFSFUNKTION: Matplotlib-Figure → base64-PNG (für Dash dcc.Graph)
# =============================================================================
def _fig_to_base64(fig):
    """
    Konvertiert eine Matplotlib-Figure zu einem base64-PNG.
    KEIN bbox_inches='tight' → Figure-Größe bleibt exakt wie gesetzt.
    """
    import io, base64
    import matplotlib.pyplot as plt

    buf = io.BytesIO()
    fig.savefig(
        buf,
        format='png',
        dpi=fig.get_dpi(),          # Nutze den dpi-Wert der Figure
        bbox_inches=None,           # KEIN tight – feste Größe beibehalten
        facecolor=fig.get_facecolor(),
        edgecolor='none',
        pad_inches=0,
    )
    buf.seek(0)
    img_b64 = base64.b64encode(buf.read()).decode('utf-8')
    plt.close(fig)
    return f"data:image/png;base64,{img_b64}"



# =============================================================================
# HILFSFUNKTION: Stadtcode aus Dateiname extrahieren
# =============================================================================
def extract_city_code(file_path):
    """
    Extrahiert den Stadtcode aus dem Dateinamen.

    Format erwartet: "DATUM_STADTCODE_*.fit" oder "DATUM_STADTCODE_*.gpx"
    Beispiele:
      "2025-07-30_FRA_Run_6.68Km.fit"  → "FRA"
      "2025-09-10_HH_Run_10.27Km.fit"  → "HH"
      "UnbekanntesDateiformat.fit"       → None

    Args:
        file_path (str): Vollständiger Pfad oder nur Dateiname.

    Returns:
        str | None: Stadtcode in Großbuchstaben oder None wenn nicht erkennbar.
    """
    import os
    filename = os.path.basename(file_path)          # Nur Dateiname, ohne Pfad
    name_without_ext = os.path.splitext(filename)[0]  # Ohne .fit/.gpx
    parts = name_without_ext.split('_')

    # Mindestens 2 Teile nötig: ["2025-07-30", "FRA", ...]
    if len(parts) >= 2:
        city_code = parts[1].upper().strip()
        # Plausibilitätsprüfung: 2–6 Zeichen, nur Buchstaben
        if 2 <= len(city_code) <= 6 and city_code.isalpha():
            return city_code

    return None  # Stadtcode nicht erkennbar

# =============================================================================
# HILFSFUNKTION: Alle Läufe einer Stadt laden
# =============================================================================
def load_runs_for_city(city_code, all_file_options):
    """
    Lädt alle Läufe, deren Dateiname den angegebenen Stadtcode enthält.

    Args:
        city_code (str): Stadtcode z.B. "FRA" oder "HH".
        all_file_options (list): Rückgabe von list_files() –
                                  Liste von dicts mit 'value' (Dateipfad).

    Returns:
        list[pd.DataFrame]: Liste der erfolgreich geladenen DataFrames.
        list[str]:          Liste der geladenen Dateipfade (für Debug/Titel).
    """
    loaded_dfs   = []
    loaded_paths = []

    for opt in all_file_options:
        path = opt['value']

        # Überspringe Platzhalter-Einträge
        if path in ['NO_FILES', 'NO_FOLDER', 'ERROR']:
            continue

        # Prüfe ob diese Datei zum gewünschten Stadtcode gehört
        if extract_city_code(path) == city_code.upper():
            try:
                df_run = process_selected_file(path)
                if not df_run.empty:
                    loaded_dfs.append(df_run)
                    loaded_paths.append(path)
                    print(f"  [Heatmap/{city_code}] Geladen: {path} "
                          f"({len(df_run)} Punkte)")
            except Exception as e:
                print(f"  [Heatmap/{city_code}] Fehler bei {path}: {e}")

    print(f"  [Heatmap/{city_code}] Insgesamt {len(loaded_dfs)} Läufe geladen.")
    return loaded_dfs, loaded_paths




# -----------------------------------------------------------------------------
# PIXEL-PLOT 1: HEATMAP (count) – mehrere Läufe, Linienstärke = Häufigkeit
# -----------------------------------------------------------------------------
def create_pixel_heatmap(dataframes,
                         img_width=900, img_height=900,
                         line_width=2, bg_color='#0d0d0d',
                         mode='single',
                         city_code=None,
                         n_city_runs=0,
                         highlight_df=None):
    """
    Heatmap: Zeichnet einen oder mehrere Läufe pixelweise.
    Je öfter ein Pixel überquert wurde, desto heller/wärmer die Farbe.

    Args:
        dataframes (list[pd.DataFrame] | pd.DataFrame):
            Einzelner DataFrame ODER Liste von DataFrames.
        img_width, img_height (int): Canvas-Größe in Pixel.
        line_width (int): Breite der gezeichneten Linien.
        bg_color (str): Hintergrundfarbe.
        mode (str): 'single' → ein Lauf | 'city' → alle Läufe der Stadt.
        city_code (str | None): Erkannter Stadtcode (z.B. "FRA").
        n_city_runs (int): Anzahl der geladenen Stadt-Läufe (nur für Titel).

    Returns:
        plotly.graph_objects.Figure
    """
    import numpy as np
    import matplotlib
    matplotlib.use('Agg')
    import matplotlib.pyplot as plt
    import matplotlib.cm as cm
    from matplotlib.colors import LinearSegmentedColormap, Normalize
    from mpl_toolkits.axes_grid1 import make_axes_locatable
    import pandas as pd
    import plotly.graph_objects as go

    DPI = 100  # Fester DPI-Wert – NICHT ändern

    if isinstance(dataframes, pd.DataFrame):
        dataframes = [dataframes]
    dataframes = [
        df for df in dataframes
        if not df.empty and 'lat' in df.columns and 'lon' in df.columns
    ]
    if not dataframes:
        fig = go.Figure()
        fig.update_layout(
            paper_bgcolor='#1e1e1e', font=dict(color='white'),
            height=img_height,
            title=dict(text='No Data avaiable', font=dict(color='white'))
        )
        return fig

    # Bounding-Box
    all_lats = np.concatenate([df['lat'].dropna().values for df in dataframes])
    all_lons = np.concatenate([df['lon'].dropna().values for df in dataframes])
    lat_min, lat_max = all_lats.min(), all_lats.max()
    lon_min, lon_max = all_lons.min(), all_lons.max()
    lat_range = lat_max - lat_min if lat_max != lat_min else 1e-6
    lon_range = lon_max - lon_min if lon_max != lon_min else 1e-6

    # Skalierung (Seitenverhältnis erhalten)
    padding = 50
    draw_w  = img_width  - 2 * padding
    draw_h  = img_height - 2 * padding
    scale   = min(draw_w / lon_range, draw_h / lat_range)
    offset_x = padding + (draw_w - lon_range * scale) / 2
    offset_y = padding + (draw_h - lat_range * scale) / 2

    def to_px(lats_arr, lons_arr):
        xs = np.clip((offset_x + (lons_arr - lon_min) * scale).astype(int), 0, img_width  - 1)
        ys = np.clip((offset_y + (lat_max  - lats_arr) * scale).astype(int), 0, img_height - 1)
        return xs, ys

    # Count-Grid
    count_grid = np.zeros((img_height, img_width), dtype=np.float32)
    for df in dataframes:
        lats_r = df['lat'].dropna().values
        lons_r = df['lon'].dropna().values
        if len(lats_r) < 2:
            continue
        xs, ys = to_px(lats_r, lons_r)
        np.add.at(count_grid, (ys, xs), 1)

    max_count = max(count_grid.max(), 1)
    log_max   = np.log1p(max_count)

    # ---------------------------------------------------------------
    # Matplotlib-Canvas: EXAKT img_width × img_height Pixel
    # figsize in Inch = Pixel / DPI
    # ---------------------------------------------------------------
    fig_mpl = plt.figure(figsize=(img_width / DPI, img_height / DPI), dpi=DPI)
    fig_mpl.patch.set_facecolor(bg_color)

    # Haupt-Axes: füllt die gesamte Figure (keine Ränder)
    ax = fig_mpl.add_axes([0, 0, 1, 1])   # [left, bottom, width, height] in Figure-Koordinaten
    ax.set_facecolor(bg_color)
    ax.set_xlim(0, img_width)
    ax.set_ylim(img_height, 0)
    ax.axis('off')

    # Colormap
    cmap_colors = [
        (0.00, '#1a0a00'),
        (0.20, '#7a2800'),
        (0.45, '#fc4e00'),
        (0.70, '#fcaa00'),
        (0.90, '#fde68a'),
        (1.00, '#ffffff'),
    ]
    cmap = LinearSegmentedColormap.from_list('heatmap', cmap_colors, N=256)

    # Linien zeichnen
    # Welche DataFrames werden GEZEICHNET?
    # - Region-Modus: alle
    # - Einzellauf-Modus: nur highlight_df, aber count_grid kam von allen
    draw_frames = dataframes if (mode == 'city' or highlight_df is None) else [highlight_df]

    from matplotlib.collections import LineCollection

    # (LineCollection - alle Segmente in einem Aufruf):
    for df in draw_frames:
        lats_r = df['lat'].dropna().values
        lons_r = df['lon'].dropna().values
        if len(lats_r) < 2:
            continue
        xs, ys = to_px(lats_r, lons_r)

        # Segmente als Array: shape (N-1, 2, 2)
        points   = np.array([xs, ys]).T.reshape(-1, 1, 2)
        segments = np.concatenate([points[:-1], points[1:]], axis=1)

        # Farbe pro Segment aus count_grid
        mid_xs = np.clip((xs[:-1] + xs[1:]) // 2, 0, img_width  - 1)
        mid_ys = np.clip((ys[:-1] + ys[1:]) // 2, 0, img_height - 1)
        counts = count_grid[mid_ys, mid_xs]
        norm_vals = np.log1p(counts) / log_max if log_max > 0 else np.zeros_like(counts)
        colors = cmap(norm_vals)

        lc = LineCollection(segments, colors=colors, linewidths=line_width,
                            capstyle='round', joinstyle='round')
        ax.add_collection(lc)

    # Colorbar als Inset-Axes (verändert NICHT die Figure-Größe)
    #cbar_ax = fig_mpl.add_axes([0.88, 0.10, 0.025, 0.70])  #  # verticale Position: rechts
    cbar_ax = fig_mpl.add_axes([0.15, 0.04, 0.70, 0.020]) # [left, bottom, w, h], horizontale Position: unten !!
    sm = cm.ScalarMappable(cmap=cmap, norm=Normalize(vmin=1, vmax=int(max_count)))
    sm.set_array([])
    cbar = fig_mpl.colorbar(sm, cax=cbar_ax, orientation='horizontal')
    cbar.set_label('Anzahl', color='white', fontsize=8)
    #cbar.ax.yaxis.set_tick_params(color='white', labelcolor='white', labelsize=7)
    cbar.ax.xaxis.set_tick_params(color='white', labelcolor='white', labelsize=7)

    # Titel als Text direkt in Figure-Koordinaten
    if mode == 'city' and city_code:
        title_str = f'Heatmap {city_code} · {n_city_runs} Läufe · max {int(max_count)}×'
    else:
        title_str = f'Heatmap (Einzellauf) · max {int(max_count)}× durchquert'
    fig_mpl.text(0.5, 0.97, title_str, color='white', fontsize=10,
                 ha='center', va='top', transform=fig_mpl.transFigure)

    img_b64 = _fig_to_base64(fig_mpl)

    plotly_title = (
        f'PIXEL-Heatmap · {city_code} · {n_city_runs} runs (region)'
        if mode == 'city' and city_code else 'Pixel-Heatmap · Einzellauf'
    )
    fig = go.Figure()
    fig.add_layout_image(dict(
        source=img_b64, xref='paper', yref='paper',
        x=0, y=1, sizex=1, sizey=1,
        xanchor='left', yanchor='top', layer='below'
    ))
    fig.update_layout(
        title=dict(text=plotly_title, font=dict(size=13, color='white')),
        paper_bgcolor=bg_color,     # Gleiche Farbe wie Plot → kein grauer Rand
        plot_bgcolor=bg_color,
        font=dict(color='white'),
        margin=dict(l=0, r=0, t=30, b=0),
        height=img_height,
        xaxis=dict(visible=False, range=[0, 1]),
        yaxis=dict(visible=False, range=[0, 1], scaleanchor='x'),
        uirevision='heatmap',
    )
    return fig




# -----------------------------------------------------------------------------
# PIXEL-PLOT 2: ELEVATION-MAP – Farbe zeigt Steigung/Gefälle je Segment
# -----------------------------------------------------------------------------
def create_route_elevation_map(df, height=500):
    """
    Interaktive Elevation-Map als go.Scattergl.
    Farbe: grün (bergab) → grau (flach) → rot (bergauf).
    Horizontale Colorbar unten: links min (grün) → mitte flach (grau) → rechts max (rot).
    """
    if df.empty or 'lat' not in df.columns or 'lon' not in df.columns:
        fig = go.Figure()
        fig.update_layout(paper_bgcolor='#0d0d0d', height=height,
                          title=dict(text='Keine GPS-Daten', font=dict(color='white')))
        return fig

    lats = df['lat'].dropna().values
    lons = df['lon'].dropna().values

    if 'elev' in df.columns and df['elev'].notna().sum() > 10:
        elevs_raw = df['elev'].ffill().bfill().values
        n_smooth  = max(5, min(50, len(elevs_raw) // 100))
        elevs     = np.convolve(elevs_raw, np.ones(n_smooth) / n_smooth, mode='same')
    else:
        elevs = np.zeros(len(lats))

    n = min(len(lats), len(lons), len(elevs))
    lats, lons, elevs = lats[:n], lons[:n], elevs[:n]
    delta = np.diff(elevs, prepend=elevs[0])

    abs_d      = np.abs(delta)
    nz         = abs_d[abs_d > 0]
    flat_thresh = np.percentile(nz, 20) if len(nz) > 0 else 0.05
    max_d      = max(np.percentile(abs_d, 80), flat_thresh * 2)

    dist = df['cum_dist_km'].values[:n] if 'cum_dist_km' in df.columns else np.zeros(n)

    total_up   = delta[delta >  flat_thresh].sum()
    total_down = abs(delta[delta < -flat_thresh].sum())

    # Farbe pro Punkt: normalisiert -max_d → 0 → +max_d
    # Colorscale: grün (0.0) → grau (0.5) → rot (1.0)
    norm_vals = np.clip((delta + max_d) / (2 * max_d), 0, 1)

    colorscale = [
        [0.00, '#00aa00'],
        [0.35, '#227722'],
        [0.50, '#666666'],
        [0.65, '#772222'],
        [1.00, '#ff2200'],
    ]

    fig = go.Figure()

    # Haupttrace: Route eingefärbt
    fig.add_trace(go.Scattergl(
        x=lons, y=lats,
        mode='markers',
        marker=dict(
            color=norm_vals,
            colorscale=colorscale,
            cmin=0, cmax=1,
            size=3,
            opacity=0.9,
            showscale=False,     # Colorbar kommt als separater Dummy-Trace unten
        ),
        hovertemplate=(
            '⛰ %{customdata[0]:.0f} m<br>'
            '📏 %{customdata[1]:.2f} km<extra></extra>'
        ),
        customdata=np.column_stack([elevs, dist]),
        showlegend=False,
    ))

    # Start / Ziel
    fig.add_trace(go.Scattergl(
        x=[lons[0]], y=[lats[0]], mode='markers',
        marker=dict(color='#b9fc62', size=12, symbol='circle'),
        showlegend=False, hovertemplate='Start<extra></extra>',
    ))
    fig.add_trace(go.Scattergl(
        x=[lons[-1]], y=[lats[-1]], mode='markers',
        marker=dict(color='#fca062', size=11, symbol='square'),
        showlegend=False, hovertemplate='Ziel<extra></extra>',
    ))

    # -------------------------------------------------------------------------
    # Horizontale Colorbar unten als Dummy-Scatter
    # links: grün (bergab/-max_d) | mitte: grau (flach/0) | rechts: rot (bergauf/+max_d)
    # -------------------------------------------------------------------------
    fig.add_trace(go.Scatter(
        x=[None], y=[None],
        mode='markers',
        marker=dict(
            colorscale=colorscale,
            cmin=-max_d,
            cmax=max_d,
            color=[0],
            colorbar=dict(
                orientation='h',
                x=0.5, xanchor='center',
                y=-0.08, yanchor='top',
                len=0.85, thickness=10,
                title=dict(
                    #text='Steigung (m/Punkt)',
                    side='bottom',
                    font=dict(color='white', size=11),
                ),
                tickfont=dict(color='white', size=10),
                tickvals=[-max_d, 0, max_d],
                ticktext=[
                    f'↓ min ({-max_d:.2f}m)',
                    'flach',
                    f'↑ max (+{max_d:.2f}m)',
                ],
                tickcolor='white',
            ),
            showscale=True,
        ),
        showlegend=False,
        hoverinfo='skip',
    ))

    fig.update_layout(
        title=dict(
            text=f'Elevation-Map · ↑ {total_up:.0f} m  ↓ {total_down:.0f} m  '
                 f' [green=downhill · gray=flat · red=uphill]',
            font=dict(size=12, color='white')
        ),
        xaxis=dict(visible=False, scaleanchor='y', scaleratio=1),
        yaxis=dict(visible=False),
        paper_bgcolor='#0d0d0d',
        plot_bgcolor='#0d0d0d',
        font=dict(color='white'),
        margin=dict(l=0, r=0, t=40, b=70),
        height=height,
        uirevision='elevation_map',
        dragmode='zoom',
    )
    return fig






# -----------------------------------------------------------------------------
# PIXEL-PLOT 3: PACE-MAP – Farbe zeigt Tempo je Segment (schnell = blau, langsam = rot)
# -----------------------------------------------------------------------------
def create_route_pace_map(df, height=500):
    """
    Interaktive Pace-Map als go.Scattergl.
    Horizontale Colorbar unten:
    links: rot (langsam/höchste Pace) → mitte: gelb → rechts: blau (schnell/niedrigste Pace).
    """
    if df.empty or 'lat' not in df.columns or 'lon' not in df.columns:
        fig = go.Figure()
        fig.update_layout(paper_bgcolor='#0d0d0d', height=height,
                          title=dict(text='Keine GPS-Daten', font=dict(color='white')))
        return fig

    lats = df['lat'].dropna().values
    lons = df['lon'].dropna().values

    if 'speed_kmh' in df.columns and df['speed_kmh'].notna().sum() > 10:
        speed = df['speed_kmh'].ffill().fillna(0).values
        pace  = np.where(speed > 0.5, 60.0 / speed, np.nan)
    elif 'vel_kmps' in df.columns:
        vel  = df['vel_kmps'].fillna(0).values
        pace = np.where(vel * 3600 > 0.5, 60.0 / (vel * 3600), np.nan)
    else:
        pace = np.full(len(lats), np.nan)

    n = min(len(lats), len(lons), len(pace))
    lats, lons, pace = lats[:n], lons[:n], pace[:n]
    dist = df['cum_dist_km'].values[:n] if 'cum_dist_km' in df.columns else np.zeros(n)

    valid = pace[(pace >= 2) & (pace <= 15) & ~np.isnan(pace)]
    p5    = np.percentile(valid, 5)  if len(valid) > 0 else 4.0
    p95   = np.percentile(valid, 95) if len(valid) > 0 else 8.0
    avg   = valid.mean()             if len(valid) > 0 else 6.0

    # norm: 0 = schnell (blau), 1 = langsam (rot)
    # Colorscale links → rechts: rot (langsam) → gelb → grün → blau (schnell)
    # Daher invertieren: norm_inv = 1 - norm → 0 = langsam (rot links), 1 = schnell (blau rechts)
    norm     = np.clip((pace - p5) / (p95 - p5), 0, 1)
    # 1 - norm: schnell (niedrige Pace) → 1 → blau (rechts in Colorscale)
    #           langsam (hohe Pace)     → 0 → rot  (links in Colorscale)
    norm_inv = np.where(np.isnan(norm), 0.5, 1 - norm)

    # Colorscale: rot (0/links/langsam) → orange → gelb → grün → blau (1/rechts/schnell)
    colorscale = [
        [0.00, '#cc0000'],
        [0.25, '#ff6600'],
        [0.50, '#ffcc00'],
        [0.75, '#00cc88'],
        [1.00, '#0033cc'],
    ]

    def fmt_pace(p):
        if np.isnan(p): return '–'
        m = int(p); s = int(round((p - m) * 60))
        return f'{m}:{s:02d} min/km'

    m_avg = int(avg); s_avg = int(round((avg - m_avg) * 60))

    fig = go.Figure()

    fig.add_trace(go.Scattergl(
        x=lons, y=lats,
        mode='markers',
        marker=dict(
            color=norm_inv,
            colorscale=colorscale,
            cmin=0, cmax=1,
            size=3,
            opacity=0.9,
            showscale=False,
        ),
        hovertemplate=(
            '🏃 %{customdata[0]}<br>'
            '📏 %{customdata[1]:.2f} km<extra></extra>'
        ),
        customdata=np.column_stack([
            [fmt_pace(p) for p in pace],
            dist,
        ]),
        showlegend=False,
    ))

    fig.add_trace(go.Scattergl(
        x=[lons[0]], y=[lats[0]], mode='markers',
        marker=dict(color='#b9fc62', size=12, symbol='circle'),
        showlegend=False, hovertemplate='Start<extra></extra>',
    ))
    fig.add_trace(go.Scattergl(
        x=[lons[-1]], y=[lats[-1]], mode='markers',
        marker=dict(color='#fca062', size=11, symbol='square'),
        showlegend=False, hovertemplate='Ziel<extra></extra>',
    ))

    # -------------------------------------------------------------------------
    # Horizontale Colorbar unten
    # Angezeigter Bereich: p95 (rot/langsam) links → p5 (blau/schnell) rechts
    # -------------------------------------------------------------------------
    fig.add_trace(go.Scatter(
        x=[None], y=[None],
        mode='markers',
        marker=dict(
            colorscale=colorscale,
            cmin=0,    # 0 = rot = langsam
            cmax=1,    # 1 = blau = schnell
            color=[0],
            colorbar=dict(
                orientation='h',
                x=0.5, xanchor='center',
                y=-0.08, yanchor='top',
                len=0.85, thickness=10,
                title=dict(
                    #text='Pace (min/km)  —  links: langsam · rechts: schnell',
                    side='bottom',
                    font=dict(color='white', size=11),
                ),
                tickfont=dict(color='white', size=10),
                tickvals=[0, 0.5, 1],          # im cmin/cmax Bereich → werden angezeigt
                ticktext=[
                    fmt_pace(p95),             # 0 = langsam = rot = links
                    fmt_pace((p5 + p95) / 2),  # 0.5 = mittel
                    fmt_pace(p5),              # 1 = schnell = blau = rechts
                ],
                tickcolor='white',
            ),
            showscale=True,
        ),
        showlegend=False,
        hoverinfo='skip',
    ))

    fig.update_layout(
        title=dict(
            text=f'Pace-Map · Ø {m_avg}:{s_avg:02d} min/km  [red=slow · blue=fast]',
            font=dict(size=12, color='white')
        ),
        xaxis=dict(visible=False, scaleanchor='y', scaleratio=1),
        yaxis=dict(visible=False),
        paper_bgcolor='#0d0d0d',
        plot_bgcolor='#0d0d0d',
        font=dict(color='white'),
        margin=dict(l=0, r=0, t=40, b=70),
        height=height,
        uirevision='pace_map',
        dragmode='zoom',
    )
    return fig




# -----------------------------------------------------------------------------
# PIXEL-PLOT 4: HEART-RATE-MAP
# -----------------------------------------------------------------------------
def create_route_hr_map(df, mode='bpm', height=500):
    """
    Interaktive HR-Map als go.Scattergl.
    mode='bpm'  → Farbe: dunkelrot (niedrig BPM) → gelb/weiß (max BPM)
                  Horizontale Colorbar unten: links dunkelrot → rechts hellgelb
    mode='zone' → Farbe: je HR-Zone (blau/grün/gelb/orange/rot)
                  Legende rechts mit Zonen-Namen und Prozentanteilen
    """
    if df.empty or 'lat' not in df.columns or 'lon' not in df.columns:
        fig = go.Figure()
        fig.update_layout(paper_bgcolor='#0d0d0d', height=height,
                          title=dict(text='No GPS-Data', font=dict(color='white')))
        return fig

    lats  = df['lat'].dropna().values
    lons  = df['lon'].dropna().values
    has_hr = ('heart_rate' in df.columns and df['heart_rate'].notna().sum() > 10)
    hr_raw = df['heart_rate'].ffill().bfill().values if has_hr else np.full(len(lats), np.nan)

    n = min(len(lats), len(lons), len(hr_raw))
    lats, lons, hr = lats[:n], lons[:n], hr_raw[:n]
    dist = df['cum_dist_km'].values[:n] if 'cum_dist_km' in df.columns else np.zeros(n)

    valid_hr = hr[(hr > 40) & (hr < 220) & ~np.isnan(hr)]
    hr_min   = np.percentile(valid_hr, 2)  if len(valid_hr) > 0 else 100
    hr_max   = np.percentile(valid_hr, 98) if len(valid_hr) > 0 else 180
    hr_avg   = valid_hr.mean()             if len(valid_hr) > 0 else 0

    fig = go.Figure()

    if mode == 'bpm':
        # Colorscale: dunkelrot (niedrig) → rot → orange → gelb → hellgelb (max)
        colorscale_bpm = [
            [0.00, '#3d0000'],
            [0.25, '#8b0000'],
            [0.50, '#cc2200'],
            [0.70, '#ff6600'],
            [0.85, '#ffcc00'],
            [1.00, '#ffff99'],
        ]

        fig.add_trace(go.Scattergl(
            x=lons, y=lats,
            mode='markers',
            marker=dict(
                color=hr,
                colorscale=colorscale_bpm,
                cmin=hr_min,
                cmax=hr_max,
                size=3,
                opacity=0.9,
                showscale=False,
            ),
            hovertemplate=(
                '❤️ %{customdata[0]:.0f} bpm<br>'
                '📏 %{customdata[1]:.2f} km<extra></extra>'
            ),
            customdata=np.column_stack([hr, dist]),
            showlegend=False,
        ))

        # -------------------------------------------------------------------------
        # Horizontale Colorbar unten: links dunkelrot (min BPM) → rechts hellgelb (max BPM)
        # -------------------------------------------------------------------------
        fig.add_trace(go.Scatter(
            x=[None], y=[None],
            mode='markers',
            marker=dict(
                colorscale=colorscale_bpm,
                cmin=hr_min,
                cmax=hr_max,
                color=[0],
                colorbar=dict(
                    orientation='h',
                    x=0.5, xanchor='center',
                    y=-0.08, yanchor='top',
                    len=0.85, thickness=10,
                    title=dict(
                        #text='Heart Rate (bpm)  —  links: niedrig · rechts: max',
                        side='bottom',
                        font=dict(color='white', size=11),
                    ),
                    tickfont=dict(color='white', size=10),
                    tickvals=[hr_min, (hr_min + hr_max) / 2, hr_max],
                    ticktext=[
                        f'{hr_min:.0f} bpm',
                        f'{(hr_min + hr_max) / 2:.0f} bpm',
                        f'{hr_max:.0f} bpm',
                    ],
                    tickcolor='white',
                ),
                showscale=True,
            ),
            showlegend=False,
            hoverinfo='skip',
        ))

    else:
        # Zone-Modus: Farbe nach HR-Zone
        zone_colors = [get_zone_for_bpm(h) for h in hr]
        colors = [
            HR_ZONES[z]['color'] if not np.isnan(h) and h > 40 else '#1a1a1a'
            for h, z in zip(hr, zone_colors)
        ]

        fig.add_trace(go.Scattergl(
            x=lons, y=lats,
            mode='markers',
            marker=dict(color=colors, size=3, opacity=0.9),
            hovertemplate=(
                '❤️ %{customdata[0]:.0f} bpm · Zone %{customdata[1]}<br>'
                '📏 %{customdata[2]:.2f} km<extra></extra>'
            ),
            customdata=np.column_stack([
                hr,
                [get_zone_for_bpm(h) + 1 if not np.isnan(h) and h > 40 else 0 for h in hr],
                dist,
            ]),
            showlegend=False,
        ))

        # Zonen-Legende
        for z in HR_ZONES:
            cnt = np.sum((hr >= z['lower']) & (hr < z['upper']) & ~np.isnan(hr))
            pct = 100 * cnt / len(valid_hr) if len(valid_hr) > 0 else 0
            fig.add_trace(go.Scatter(
                x=[None], y=[None], mode='markers',
                marker=dict(color=z['color'], size=10),
                name=f'{z["name"]} {z["label"]}  {pct:.1f}%',
                showlegend=True,
            ))

    # Start / Ziel
    fig.add_trace(go.Scattergl(
        x=[lons[0]], y=[lats[0]], mode='markers',
        marker=dict(color='#b9fc62', size=12, symbol='circle'),
        showlegend=False, hovertemplate='Start<extra></extra>',
    ))
    fig.add_trace(go.Scattergl(
        x=[lons[-1]], y=[lats[-1]], mode='markers',
        marker=dict(color='#fca062', size=11, symbol='square'),
        showlegend=False, hovertemplate='Ziel<extra></extra>',
    ))

    no_hr = '' if has_hr else ' · No HR-Data'
    mode_label = 'BPM-intensity [red=low · white=high]' if mode == 'bpm' else 'Zones'

    fig.update_layout(
        title=dict(
            text=f'Heart Rate ({mode_label}) · Ø {hr_avg:.0f} bpm{no_hr}',
            font=dict(size=12, color='white')
        ),
        xaxis=dict(visible=False, scaleanchor='y', scaleratio=1),
        yaxis=dict(visible=False),
        paper_bgcolor='#0d0d0d',
        plot_bgcolor='#0d0d0d',
        font=dict(color='white'),
        margin=dict(l=0, r=0 if mode == 'bpm' else 130, t=40, b=70),
        height=height,
        legend=dict(
            x=1.01, y=0.5, xanchor='left',
            font=dict(color='white', size=9),
            bgcolor='rgba(0,0,0,0.4)',
        ) if mode == 'zone' else dict(visible=False),
        uirevision='hr_map',
        dragmode='zoom',
    )
    return fig







# -----------------------------------------------------------------------------
# ELEVATION-PLOT:
# -----------------------------------------------------------------------------
def create_elevation_plot(df, smooth_points=500):
    x = df['time']
    y = df['rel_elev']

    y_smooth = (
        pd.Series(y.values)
        .interpolate(method='linear', limit=10, limit_direction='both')  # kurze Lücken schließen
        .rolling(window=15, center=True, min_periods=1)
        .mean()
        .values
    )
    x_smooth = x

    fig = go.Figure()

    # Separate Behandlung für positive und negative Bereiche
    y_array = np.array(y_smooth)
    x_array = np.array(x_smooth)

    # Positive Bereiche (Anstiege) - Gradient von transparent unten zu grün oben
    positive_mask = y_array >= 0
    if np.any(positive_mask):
        # Nulllinie für positive Bereiche
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.zeros_like(y_array),
            mode='lines',
            line=dict(width=0),
            hoverinfo='skip',
            showlegend=False
        ))

        # Positive Bereiche mit Gradient nach oben
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.where(y_array >= 0, y_array, 0),  # Nur positive Werte
            fill='tonexty',  # Fill zur vorherigen Trace (Nulllinie)
            mode='lines',
            line=dict(width=0),
            fillgradient=dict(
                type="vertical",
                colorscale=[
                    (0.0, "rgba(17, 17, 17, 0.0)"),    # Transparent unten (bei y=0)
                    (1.0, "rgba(43, 82, 144, 0.8)")     # Blau oben (bei maximaler Höhe)  Grün: 73, 189, 115
                ]
            ),
            hoverinfo='skip',
            showlegend=False
        ))

    # Negative Bereiche (Abstiege) - Gradient von grün unten zu transparent oben
    negative_mask = y_array < 0
    if np.any(negative_mask):
        # Nulllinie für negative Bereiche
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.where(y_array < 0, y_array, 0),  # Nur negative Werte
            mode='lines',
            line=dict(width=0),
            hoverinfo='skip',
            showlegend=False
        ))

        # Negative Bereiche mit Gradient nach unten
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.zeros_like(y_array),
            fill='tonexty',  # Fill zur vorherigen Trace (negative Werte)
            mode='lines',
            line=dict(width=0),
            fillgradient=dict(
                type="vertical",
                colorscale=[
                    (0.0, "rgba(43, 82, 144, 0.8)"),    # Blau unten (bei minimaler Tiefe)
                    (1.0, "rgba(17, 17, 17, 0.0)")     # Transparent oben (bei y=0)
                ]
            ),
            hoverinfo='skip',
            showlegend=False
        ))

    # Hauptlinie (geglättet) - über allem
    fig.add_trace(go.Scatter(
        x=x_smooth,
        y=y_smooth,
        mode='lines',
        line=dict(color='#2b5290', width=2),  # Weiße Linie für bessere Sichtbarkeit
        hovertemplate='Time: %{x|%H:%M:%S}<br>Δ Elevation = %{y:.1f} m<extra></extra>',
        #name='Elevation',
        showlegend=False
    ))

    # Add horizontal reference line at y=0
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=0,
        y1=0,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )

    # Layout im Dark Theme
    fig.update_layout(
        title=dict(text='Elevation Profile (relative to start: 0 m)', font=dict(size=16, color='white')),
        xaxis_title='Time',
        yaxis_title='Relative Elevation (m)',
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=50, b=40),
        height=400,
        uirevision='constant',  # Avoiding not needed Re-renderings
    )

    return fig

# Alte Version - normaler fill between:
# def create_elevation_plot(df, smooth_points=500):
#     # Originale Daten
#     x = df['time']
#     y = df['rel_elev']
#
#     # Einfache Glättung: nur Y-Werte glätten, X-Werte beibehalten
#     if len(y) >= 4:  # Genug Punkte für cubic interpolation
#         y_numeric = y.to_numpy()
#
#         # Nur gültige Y-Punkte für Interpolation
#         mask = ~np.isnan(y_numeric)
#
#         if np.sum(mask) >= 4:  # Genug gültige Punkte
#             # Index-basierte Interpolation für Y-Werte
#             valid_indices = np.where(mask)[0]
#             valid_y = y_numeric[mask]
#
#             # Interpolation über die Indizes
#             f = interp1d(valid_indices, valid_y, kind='cubic',
#                         bounds_error=False, fill_value='extrapolate')
#
#             # Neue Y-Werte für alle ursprünglichen X-Positionen
#             all_indices = np.arange(len(y))
#             y_smooth = f(all_indices)
#
#             # Originale X-Werte beibehalten
#             x_smooth = x
#         else:
#             # Fallback: originale Daten
#             x_smooth, y_smooth = x, y
#     else:
#         # Zu wenige Punkte: originale Daten verwenden
#         x_smooth, y_smooth = x, y
#
#     fig = go.Figure()
#
#     # Fläche unter der Kurve (mit geglätteten Daten)
#     fig.add_trace(go.Scatter(
#         x=x_smooth, y=y_smooth,
#         mode='lines',
#         line=dict(color='#1CAF50'),      # Fill between color!
#         fill='tozeroy',
#         #fillcolor='rgba(226, 241, 248)',
#         hoverinfo='skip',
#         showlegend=False
#     ))
#
#     # Hauptlinie (geglättet)
#     fig.add_trace(go.Scatter(
#         x=x_smooth, y=y_smooth,
#         mode='lines',
#         line=dict(color='#084C20', width=2), # Line color!
#         name='Elevation',
#         showlegend=False
#     ))
#
#     # SUPDER IDEE, ABER GEHT NICHT WEGE NEUEN smoothed POINTS! GEHT NUR BEI X
#     #fig.update_traces(
#     #    hovertemplate=(
#     #        #"Time: %{customdata[0]}<br>" +
#     #        "Distance (km): %{customdata[0]:.2f}<br>" +
#     #        "Elevation: %{customdata[1]}<extra></extra>" +
#     #        "Elapsed Time: %{customdata[2]}<extra></extra>"
#     #    ),
#     #    customdata=df[['cum_dist_km','elev', 'time']]
#     #
#
#     # Layout im Dark Theme
#     fig.update_layout(
#         title=dict(text='Höhenprofil relativ zum Startwert', font=dict(size=16, color='white')),
#         xaxis_title='Zeit',
#         yaxis_title='Höhe relativ zum Start (m)',
#         template='plotly_dark',
#         paper_bgcolor='#1e1e1e',
#         plot_bgcolor='#111111',
#         font=dict(color='white'),
#         margin=dict(l=40, r=40, t=50, b=40),
#         height=400
#     )
#
#     return fig
# #####################



# -----------------------------------------------------------------------------
# SPEED-PLOT:
# -----------------------------------------------------------------------------
def create_speed_plot(df):
    mask = df['speed_kmh_smooth'].isna()
    mean_speed_kmh = df['speed_kmh'].mean()
    fig = go.Figure()
    fig.add_trace(go.Scatter(
        x=df['time'][~mask],
        y=df['speed_kmh_smooth'][~mask],
        mode='lines',
        name='Geglättete Geschwindigkeit',
        line=dict(color='royalblue'),
        hovertemplate='Time: %{x|%H:%M:%S}<br>Speed = %{y:.1f} km/h<extra></extra>',
    ))
    fig.update_layout(
        title=dict(text=f'Speed over Time (smoothed) - Ø {mean_speed_kmh:.2f} km/h', font=dict(size=16)),
        xaxis=dict(title='Time', tickformat='%H:%M', type='date'),
        yaxis=dict(title='Speed (km/h)', rangemode='tozero'),
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=40, b=40),
        uirevision='constant',  # Avoiding not needed Re-renderings
    )
    # Add horizontal reference line at y=mean_speed_kmh
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=mean_speed_kmh,
        y1=mean_speed_kmh,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )
    return fig



# -----------------------------------------------------------------------------
# DEVIATION-PLOT:  Distanz-Zeit-Diagramm
# -----------------------------------------------------------------------------
def create_deviation_plot(df):
    # Compute mean velocity in km/s
    vel_kmps_mean = df['cum_dist_km'].iloc[-1] / df['time_diff_sec'].iloc[-1]
    # Expected cumulative distance assuming constant mean velocity
    df['cum_dist_km_qmean'] = df['time_diff_sec'] * vel_kmps_mean
    # Deviation from mean velocity distance
    df['del_dist_km_qmean'] = df['cum_dist_km'] - df['cum_dist_km_qmean']
    # Plot the deviation
    fig = go.Figure()
    fig.add_trace(go.Scatter(
        x=df['time_loc'],
        y=df['del_dist_km_qmean'],
        mode='lines',
        name='Δ Strecke (km)',
        line=dict(color='royalblue'),
        hovertemplate='Time: %{x|%H:%M:%S}<br>Δ Strecke = %{y:.2f} km<extra></extra>',
    ))
    fig.update_layout(
        title=dict(text='Distance deviation from integrated average pace', font=dict(size=16)),
        yaxis_title='Δ Distance (km)',
        xaxis_title='Time',
        template='plotly_dark',
        height=400,
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white', size=14),
        margin=dict(l=40, r=40, t=50, b=40),
        uirevision='constant',  # Avoiding not needed Re-renderings
    )
    # Add horizontal reference line at y=0
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=0,
        y1=0,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )
    return fig



# -----------------------------------------------------------------------------
# HEART-RATE-PLOT:
# -----------------------------------------------------------------------------
def create_heart_rate_plot(df):
    # Maske für gültige Heart Rate Daten
    mask = df['hr_smooth'].isna()

    # Durchschnittliche Heart Rate berechnen (nur gültige Werte)
    valid_hr = df['heart_rate'].dropna()
    if len(valid_hr) > 0:
        mean_hr = valid_hr.mean()
        min_hr = valid_hr.min()
        max_hr = valid_hr.max()
    else:
        mean_hr = 0
        min_hr = 0
        max_hr = 0

    fig = go.Figure()

    # Heart Rate Linie (geglättet)
    mask = df['heart_rate'].isna()
    fig.add_trace(go.Scatter(
        x=df['time'][~mask],
        y=df['heart_rate'][~mask],
        mode='lines',
        line=dict(color='#ff1d34', width=1.5),   # etwas dünner für gezackte Linie
        showlegend=False,
        hovertemplate=(
            "Time: %{x}<br>" +
            "Heart Rate: %{y:.0f} bpm<br>" +
            "<extra></extra>"
        )
    ))

    # # Optional: Raw Heart Rate als dünnere, transparente Linie
    # if not df['heart_rate'].isna().all():
    #     fig.add_trace(go.Scatter(
    #         x=df['time'],
    #         y=df['heart_rate'],
    #         mode='lines',
    #         name='Raw Herzfrequenz',
    #         line=dict(color='#E43D70', width=1, dash='dot'),
    #         opacity=0.3,
    #         showlegend=False,
    #         hoverinfo='skip'
    #     ))

    # Durchschnittslinie
    if mean_hr > 0:
        fig.add_shape(
            type='line',
            x0=df['time_loc'].iloc[0],
            x1=df['time_loc'].iloc[-1],
            y0=mean_hr,
            y1=mean_hr,
            line=dict(color='gray', width=1, dash='dash'),
        )

        # Annotation für Durchschnittswert
        fig.add_annotation(
            x=df['time_loc'].iloc[int(len(df) * 0.5)],  # Bei 50% der Zeit
            y=mean_hr,
            text=f"Ø {mean_hr:.0f} bpm",
            showarrow=True,
            arrowhead=2,
            arrowcolor="gray",
            bgcolor="rgba(128,128,128,0.1)",
            bordercolor="gray",
            font=dict(color="white", size=10)
        )

    # Heart Rate Zonen (optional)
    if mean_hr > 0:
        # Geschätzte maximale Herzfrequenz (Beispiel: 200 bpm)
        max_hr_estimated = 200  # oder z. B. 220 - alter

        ## Definiere feste HR-Zonen in BPM
        #zones = [
        #    {"name": "Zone 0", "lower": 0,   "upper": 40,  "color": "#333333"}, # Unrealistischer Wertebereich
        #    {"name": "Zone 1", "lower": 40,  "upper": 124, "color": "#4A4A4A"}, # Regeneration (Recovery) (#111111 Transparent)
        #    {"name": "Zone 2", "lower": 124, "upper": 154, "color": "#87CEFA"}, # Grundlagenausdauer (Endurance)
        #    {"name": "Zone 3", "lower": 154, "upper": 169, "color": "#90EE90"}, # Tempo (Aerob)
        #    {"name": "Zone 4", "lower": 169, "upper": 184, "color": "#FFDAB9"}, # Schwelle (Threshold) (Anaerob)
        #    {"name": "Zone 5", "lower": 184, "upper": max_hr_estimated, "color": "#FFB6C1"}, # Neuromuskulär (Neuromuskulär)
        #]
        #zones = [
        #    {"name": "Zone 0", "lower": 0,   "upper": 40,  "color": "#2b2b2b"},   # unrealistisch / very dark neutral
        #    {"name": "Zone 1", "lower": 40,  "upper": 124, "color": "#7FB3FF"},   # Pastellblau (Recovery)
        #    {"name": "Zone 2", "lower": 124, "upper": 154, "color": "#9DE79A"},   # Pastellgrün (Endurance - Grundlagenausdauer)
        #    {"name": "Zone 3", "lower": 154, "upper": 169, "color": "#FFF29A"},   # Pastellgelb (Aerob - Tempo)
        #    {"name": "Zone 4", "lower": 169, "upper": 184, "color": "#FFBE7A"},   # Pastellorange (Anaerob - Threshold - Schwelle)
        #    {"name": "Zone 5", "lower": 184, "upper": max_hr_estimated, "color": "#FF9AA2"}, # Pastellrot (Neuromuskulär)
        #]
        zones = HR_ZONES

        # Berechne die Anzahl der Werte in jeder Zone
        total_count = len(valid_hr)
        for zone in zones:
            # Filter für die Zone
            zone_count = valid_hr[(valid_hr >= zone["lower"]) & (valid_hr < zone["upper"])].count()
            zone_percentage = (zone_count / total_count) * 100 if total_count > 0 else 0

            # Zeichne die Zone als Hintergrund
            fig.add_hrect(
                y0=zone["lower"], y1=zone["upper"],
                fillcolor=zone["color"],
                opacity=0.15,
                line_width=0,
            )

            # Annotation für die Zone (Name und Prozentsatz)
            fig.add_annotation(
                x=df['time_loc'].iloc[-1],  # Rechts am Plot
                y=zone["upper"] -6 ,  # Oben in der Zone
                #y=(zone["lower"] + zone["upper"]) / 2,   # Falls Pos. mittig je Zone gewünscht
                text=f"{zone['name']}<br>{zone_percentage:.1f}%",
                showarrow=False,
                font=dict(color="white", size=10),
                align="left",
                bgcolor="rgba(0,0,0,0.5)",
                bordercolor="gray",
            )

    # Layout
    title_text = f'Heart Rate over Time:'
    if mean_hr > 0:
        title_text += f' Ø {mean_hr:.0f} bpm (Range: {min_hr:.0f}-{max_hr:.0f})'

    fig.update_layout(
        title=dict(text=title_text, font=dict(size=16, color='white')),
        xaxis=dict(
            title='Time',
            tickformat='%H:%M',
            type='date'
        ),
        yaxis=dict(
            title='Heart Rate (bpm)',
            range=[70, 200]  # instead of: rangemode='tozero'
        ),
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=50, b=40),
        height=400,
        uirevision='constant',  # Avoiding not needed Re-renderings
    )

    return fig



# -----------------------------------------------------------------------------
# PACE-BAR-PLOT:
# -----------------------------------------------------------------------------
def create_pace_bars_plot(df, formatted_pace=None):
    """
    Strava-Style Pace-Histogram: Horizontale Balken, ein Balken pro km-Segment.
    Links: km-Label + Pace-Text. Mitte: Balken (Breite = Pace-Wert).
    Rechts: Elevation-Delta und Heart Rate je Segment.
    Vertikale gestrichelte Linie = Durchschnittspace.
    """
    import pandas as pd
    import numpy as np
    import plotly.graph_objects as go

    # Sicherstellen dass time eine datetime-Spalte ist
    if not pd.api.types.is_datetime64_any_dtype(df['time']):
        df = df.copy()
        df['time'] = pd.to_datetime(df['time'], errors='coerce')

    df = df.copy()
    df['km'] = df['cum_dist_km'].astype(int)
    df['time_sec'] = (df['time'] - df['time'].iloc[0]).dt.total_seconds()

    # -------------------------------------------------------------------------
    # Pace, Elevation-Delta, HR je km-Segment berechnen
    # -------------------------------------------------------------------------
    segments = []
    for km_val, group in df.groupby('km'):
        dist_start  = group['cum_dist_km'].iloc[0]
        dist_end    = group['cum_dist_km'].iloc[-1]
        segment_len = dist_end - dist_start

        time_start       = group['time_sec'].iloc[0]
        time_end         = group['time_sec'].iloc[-1]
        elapsed_time_sec = time_end - time_start

        if segment_len > 0 and elapsed_time_sec > 0:
            pace_min = (elapsed_time_sec / 60) / segment_len
        else:
            pace_min = np.nan

        # Elevation-Delta für dieses Segment
        elev_delta = np.nan
        if 'elev' in group.columns and group['elev'].notna().sum() >= 2:
            elev_delta = group['elev'].iloc[-1] - group['elev'].iloc[0]
        elif 'rel_elev' in group.columns and group['rel_elev'].notna().sum() >= 2:
            elev_delta = group['rel_elev'].iloc[-1] - group['rel_elev'].iloc[0]

        # Durchschnittliche HR für dieses Segment
        hr_mean = np.nan
        if 'heart_rate' in group.columns:
            valid = group['heart_rate'].dropna()
            if len(valid) > 0:
                hr_mean = valid.mean()

        # Km-Label: letzter Km erhält tatsächliche Distanz als Label
        is_last   = (km_val == df['km'].max())
        km_label  = f"{dist_end:.1f}" if is_last else str(km_val + 1)
        # Ersten Km explizit auf "1" setzen auch wenn km_val=0
        if km_val == 0 and not is_last:
            km_label = "1"

        segments.append({
            'km_val':     km_val,
            'km_label':   km_label,
            'segment_len': segment_len,
            'pace_min':   pace_min,
            'elev_delta': elev_delta,
            'hr_mean':    hr_mean,
        })

    seg_df = pd.DataFrame(segments)
    seg_df = seg_df[seg_df['pace_min'] < 20] # Pausen/Ausreißer raus
    seg_df = seg_df.dropna(subset=['pace_min'])
    seg_df = seg_df.sort_values('km_val').reset_index(drop=True)

    if seg_df.empty:
        fig = go.Figure()
        fig.update_layout(paper_bgcolor='#1e1e1e',
                          title=dict(text='Keine Pace-Daten', font=dict(color='white')))
        return fig

    # -------------------------------------------------------------------------
    # Durchschnittspace berechnen
    # -------------------------------------------------------------------------
    total_distance_km = df['cum_dist_km'].iloc[-1]
    total_seconds     = df['time_diff_sec'].iloc[-1]

    if total_distance_km > 0:
        pace_sec_per_km  = total_seconds / total_distance_km
        avg_pace_numeric = pace_sec_per_km / 60
        pace_min_i       = int(pace_sec_per_km // 60)
        pace_sec_i       = int(pace_sec_per_km % 60)
        formatted_pace   = f"{pace_min_i}:{pace_sec_i:02d} min/km"
    else:
        avg_pace_numeric = seg_df['pace_min'].mean()
        formatted_pace   = "N/A"

    # -------------------------------------------------------------------------
    # Pace → Formatierung als "M:SS"
    # -------------------------------------------------------------------------
    def fmt_pace(p):
        if pd.isna(p):
            return ""
        m = int(p)
        s = int(round((p - m) * 60))
        return f"{m}:{s:02d}"

    # -------------------------------------------------------------------------
    # Y-Achse: Segment-Labels (von oben = km 1 nach unten = letztes km)
    # Strava zeigt älteste Km oben, letzte unten → umgekehrte Reihenfolge
    # -------------------------------------------------------------------------
    # Alle Listen aus demselben sortierten Index ziehen
    y_labels  = seg_df['km_label'].tolist()   # ["1","2",...,"11","0.2"]
    pace_vals = seg_df['pace_min'].tolist()
    elev_vals = seg_df['elev_delta'].tolist()
    hr_vals   = seg_df['hr_mean'].tolist()

    # Maximale Pace für X-Achse (leicht über Max für optischen Puffer)
    x_max = max(pace_vals) * 1.18

    # -------------------------------------------------------------------------
    # Farbe der Balken: blau wie Strava, schneller = etwas heller
    # -------------------------------------------------------------------------
    pace_min_val = min(pace_vals)
    pace_max_val = max(pace_vals)
    pace_range   = max(pace_max_val - pace_min_val, 0.01)

    bar_colors = []
    for p in pace_vals:
        # Schnellster Km = hellstes Blau, langsamster = dunkelstes Blau
        norm = (p - pace_min_val) / pace_range   # 0 = schnell, 1 = langsam
        r = int(18  + norm * 10)
        g = int(85  + norm * 20)
        b = int(149 + norm * 30)
        bar_colors.append(f'rgb({r},{g},{b})')

    fig = go.Figure()

    # -------------------------------------------------------------------------
    # Balken (horizontal)
    # -------------------------------------------------------------------------
    fig.add_trace(go.Bar(
        x=pace_vals,
        y=list(range(len(y_labels))),   # ← 0,1,2,3... statt Strings  y=y_labels,
        orientation='h',
        marker=dict(
            color=bar_colors,
            line=dict(width=0),
        ),
        opacity=0.92,
        width=0.72,
        text=[fmt_pace(p) for p in pace_vals],
        textposition='inside',
        textfont=dict(color='white', size=11),
        hovertemplate=(
            'km %{y}<br>'
            'Pace: %{text}<br>'
            '<extra></extra>'
        ),
        name='',
        showlegend=False,
    ))

    # -------------------------------------------------------------------------
    # Durchschnitts-Linie (vertikal, gestrichelt)
    # -------------------------------------------------------------------------
    fig.add_shape(
        type='line',
        x0=avg_pace_numeric, x1=avg_pace_numeric,
        y0=-0.5, y1=len(y_labels) - 0.5,
        line=dict(color='rgba(180,180,180,0.7)', width=1.5, dash='dash'),
        layer='above',
    )
    fig.add_annotation(
        x=avg_pace_numeric,
        y=len(y_labels) - 0.5,
        text=f"Ø {formatted_pace}",
        showarrow=False,
        yanchor='bottom',
        font=dict(color='rgba(180,180,180,0.9)', size=10),
        bgcolor='rgba(0,0,0,0)',
    )

    # -------------------------------------------------------------------------
    # Elevation-Delta als Annotation rechts vom Balken
    # -------------------------------------------------------------------------
    has_elev = any(not np.isnan(e) for e in elev_vals)
    has_hr   = any(not np.isnan(h) for h in hr_vals)

    for i in range(len(seg_df)):
        km_lbl = y_labels[i]
        elev   = elev_vals[i]
        hr     = hr_vals[i]

        right_text = ''
        if has_elev and not np.isnan(elev):
            arrow = '↑' if elev > 0.5 else ('↓' if elev < -0.5 else '—')
            right_text += f"<span style='color:#aaaaaa'>{arrow}{abs(elev):.0f}m</span>"

        if has_hr and not np.isnan(hr):
            if right_text:
                right_text += '  '
            right_text += f"<span style='color:#ff6b6b'>♥ {hr:.0f}</span>"

        if right_text:
            fig.add_annotation(
                x=x_max,
                y=i,            # ← numerischer Index statt km_lbl String
                text=right_text,
                showarrow=False,
                xanchor='right',
                font=dict(size=12),
                bgcolor='rgba(0,0,0,0)',
            )

    # -------------------------------------------------------------------------
    # Layout
    # -------------------------------------------------------------------------
    # Höhe dynamisch: ~32px pro Balken, mindestens 300px
    plot_height = max(300, len(y_labels) * 36 + 80)

    fig.update_layout(
        title=dict(
            text=f'Pace per km · Ø {formatted_pace}',
            font=dict(size=15, color='white')
        ),
        xaxis=dict(
            title='Pace (min/km)',
            range=[0, x_max],
            tickmode='array',
            tickvals=[i * 0.5 for i in range(int(x_max / 0.5) + 2)],
            ticktext=[fmt_pace(i * 0.5) for i in range(int(x_max / 0.5) + 2)],
            showgrid=True,
            gridcolor='rgba(255,255,255,0.07)',
            zeroline=False,
            color='white',
        ),
        yaxis=dict(
            title='Distance (km)',
            autorange='reversed',     # km 1 oben, letzter km unten (wie Strava)
            tickmode='array',
            tickvals=list(range(len(y_labels))),   # ← 0,1,2...
            ticktext=y_labels,                      # ← "1","2",...,"0.4"
            tickfont=dict(size=11, color='white'),
            showgrid=False,
            zeroline=False,
        ),
        template='plotly_dark',
        height=plot_height,
        margin=dict(l=40, r=40, t=45, b=45),
        plot_bgcolor='#111111',
        paper_bgcolor='#1e1e1e',
        font=dict(color='white'),
        bargap=0.15,
        uirevision='constant',
    )

    return fig





# === App Setup ===
app = dash.Dash(__name__,
    suppress_callback_exceptions=True,         # Weniger Validierung
    compress=True,                             # Gzip-Kompression (Install:  python-flask-compress)
    external_stylesheets=[dbc.themes.SLATE],
    title = "Jogging Dashboard"
)

app.layout = html.Div([
    html.H1("Jogging Dashboard", style={'textAlign': 'center'}),
    dcc.Store(id='stored-df'),

    # Horizontales Layout für Dropdown und Button
    html.Div([
        # Linke Seite: Datei-Dropdown
        html.Div([
            html.Label("Select File:", style={'color': '#aaaaaa', 'marginBottom': '5px'}),
            dcc.Dropdown(
                id='file-dropdown',
                options=list_files(),
                value=list_files()[0]['value'],
                clearable=False,
                style={'width': '300px', 'color': 'black'}
            )
        ], style={'display': 'flex', 'flexDirection': 'column'}),

        # Rechte Seite: Export Button
        html.Div([
            html.Label("Export SVG:",
                       style={'color': '#aaaaaa', 'marginBottom': '8px'}),
            html.Button(
                [
                    html.I(className="fas fa-download"),
                    "Summary Image"
                ],
                id='export-button',
                style={
                    'backgroundColor': '#007bff',
                    'border': 'none',
                    'color': 'white',
                    'padding': '10px 12px',
                    'borderRadius': '5px',
                    'fontSize': '14px',
                    'cursor': 'pointer',
                    'display': 'flex',
                    'alignItems': 'center',
                    'justifyContent': 'center',
                    'gap': '5px'
                }
            )
        ], style={'display': 'flex', 'flexDirection': 'column', 'alignItems': 'flex-end'})

    ], style={
        'padding': '20px',
        'backgroundColor': '#1e1e1e',
        'display': 'flex',
        'justifyContent': 'space-between',  # Dropdown links, Button rechts
        'alignItems': 'flex-end',           # Beide Elemente unten ausrichten
        'minHeight': '80px'                 # Mindesthöhe für konsistentes Layout
    }),

    # Export Status
    html.Div(id='export-status', children="", style={'padding': '0 20px'}),

    # Rest deines Layouts
    html.Div(id='info-banner'),
    dcc.Graph(id='fig-map'),

    # START !!!!!!!!!!!!!!!
    # Pixel-Map Überschrift
    #html.Hr(style={'borderColor': '#111111', 'margin': '10px 20px'}),  #'#333'
    #html.Div([
    #    html.H3("Pixel-Maps", style={
    #        'color': '#aaaaaa', 'margin': '10px 0 0 0', 'fontSize': '16px'
    #    }),
    #    html.P(
    #        "Plot 1) Heatmap: Toggle between single run or all runs in the region.\n"
    #        "Plot 2), 3) & 4) Elevation, Pace & HR maps: of the currently selected run.",
    #        style={'color': '#aaaaaa', 'margin': '2px 0 8px 0', 'fontSize': '12px'}
    #    ),
    #], style={'padding': '0 20px'}),

    # Drei Plots nebeneinander – Heatmap links, Elevation Mitte, Pace rechts
    html.Div([

        # --- Heatmap-Spalte (mit Toggle darunter) ---
        html.Div([
            dcc.Graph(id='fig-pixel-heatmap'),

            # Toggle: Einzellauf ↔ Region
            html.Div([
                html.Span("Einzellauf", style={
                    'color': '#aaa', 'fontSize': '12px',
                    'marginRight': '8px', 'verticalAlign': 'middle'
                }),
                # dcc.Checklist als Toggle-Switch (ein Checkbox = ON/OFF)
                dcc.Checklist(
                    id='heatmap-mode-toggle',
                    options=[{'label': '  All runs (regional)', 'value': 'city'}],
                    #value=[],                   # Standard: leer = Einzellauf
                    value=['city'],              # Standard: Region aktiv - Jezt ist immer der Harken gesetzt!
                    inputStyle={
                        'cursor': 'pointer',
                        'width': '36px', 'height': '18px',
                        'accentColor': '#fc4e00',  # Strava-Orange
                        'verticalAlign': 'middle',
                        'marginRight': '6px',
                    },
                    labelStyle={
                        'color': '#cccccc', 'fontSize': '12px',
                        'verticalAlign': 'middle', 'cursor': 'pointer'
                    },
                ),
                # Infotext: aktuell erkannter Stadtcode
                html.Span(id='heatmap-city-info', style={
                    'color': '#fc4e00', 'fontSize': '11px',
                    'marginLeft': '12px', 'verticalAlign': 'middle'
                }),
            ], style={
                'display': 'flex', 'alignItems': 'center',
                'padding': '8px 12px',
                'backgroundColor': '#1a1a1a',
                'borderRadius': '0 0 6px 6px',
                'borderTop': '1px solid #333',
            }),
        ], style={'flex': '1', 'minWidth': '300px'}),

        # --- Elevation-Map ---
        html.Div([
            dcc.Graph(id='fig-route-elevation'),
        ], style={'flex': '1', 'minWidth': '300px'}),

        # --- Pace-Map ---
        html.Div([
            dcc.Graph(id='fig-route-pace'),
        ], style={'flex': '1', 'minWidth': '300px'}),

        # --- HR-Map (mit BPM/Zone Toggle) ---
        html.Div([
            dcc.Graph(id='fig-route-hr'),
            html.Div([
                html.Span("BPM", style={
                    'color': '#aaa', 'fontSize': '12px',
                    'marginRight': '8px', 'verticalAlign': 'middle'
                }),
                dcc.Checklist(
                    id='hr-map-mode-toggle',
                    options=[{'label': '  HR-Zonen', 'value': 'zone'}],
                    value=[],
                    inputStyle={
                        'cursor': 'pointer', 'width': '36px', 'height': '18px',
                        'accentColor': '#fc4e00', 'verticalAlign': 'middle',
                        'marginRight': '6px',
                    },
                    labelStyle={
                        'color': '#cccccc', 'fontSize': '12px',
                        'verticalAlign': 'middle', 'cursor': 'pointer'
                    },
                ),
            ], style={
                'display': 'flex', 'alignItems': 'center',
                'padding': '8px 12px',
                'backgroundColor': '#1a1a1a',
                'borderRadius': '0 0 6px 6px',
                'borderTop': '1px solid #333',
            }),
        ], style={'flex': '1', 'minWidth': '300px'}),

    ], style={
    'display': 'grid',
    'gridTemplateColumns': 'repeat(2, 1fr)',  # immer 2 Spalten
    'gap': '8px',
    'padding': '0 20px',
    'backgroundColor': '#111111'
    }),

    #html.Hr(style={'borderColor': '#111111', 'margin': '10px 20px'}),
    # ENDE !!!!!!!!!!!!!!!!

    dcc.Graph(id='fig-elevation'),
    dcc.Graph(id='fig_speed'),
    dcc.Graph(id='fig_deviation'),
    dcc.Graph(id='fig_hr'),
    dcc.Graph(id='fig_pace_bars')
])


# === Callbacks ===
# Callback 1: Load GPX File and Store as JSON
@app.callback(
    Output('stored-df', 'data'),
    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 Standard Plots
# =============================================================================
@app.callback(
    Output('info-banner', 'children'),
    Output('fig-map', 'figure', allow_duplicate=True),
    Output('fig-elevation', 'figure'),
    Output('fig_speed', 'figure'),
    Output('fig_deviation', 'figure'),
    Output('fig_hr', 'figure'),
    Output('fig_pace_bars', 'figure'),
    Input('stored-df', 'data'),
    prevent_initial_call=True
)
def update_all_plots(json_data):
    df = pd.read_json(io.StringIO(json_data), orient='split')

    info     = create_info_banner(df)
    fig_map  = create_map_plot(df)
    fig_elev = create_elevation_plot(df)
    fig_speed = create_speed_plot(df)
    fig_dev  = create_deviation_plot(df)
    fig_hr   = create_heart_rate_plot(df)
    fig_pace = create_pace_bars_plot(df)

    return (info, fig_map, fig_elev, fig_speed, fig_dev, fig_hr, fig_pace)


# =============================================================================
# Callback 2b: Update Route Pixel-Maps (Elevation + Pace)
# Separater Callback → blockiert nicht die schnellen Standard-Plots oben
# =============================================================================
@app.callback(
    Output('fig-route-elevation', 'figure'),
    Output('fig-route-pace',      'figure'),
    Input('stored-df', 'data'),
    prevent_initial_call=True
)
def update_route_plots(json_data):
    df = pd.read_json(io.StringIO(json_data), orient='split')
    return (
        create_route_elevation_map(df, height=500),
        create_route_pace_map(df,      height=500),
    )


# =============================================================================
# Callback 2c: Update HR Route-Map (mit BPM/Zone Toggle)
# =============================================================================
@app.callback(
    Output('fig-route-hr', 'figure'),
    Input('stored-df', 'data'),
    Input('hr-map-mode-toggle', 'value'),
    prevent_initial_call=True
)
def update_hr_route_map(json_data, toggle_value):
    if not json_data:
        empty = go.Figure()
        empty.update_layout(
            paper_bgcolor='#0d0d0d',
            font=dict(color='white'),
            title=dict(text='Datei wählen...', font=dict(color='white'))
        )
        return empty

    df   = pd.read_json(io.StringIO(json_data), orient='split')
    mode = 'zone' if toggle_value and 'zone' in toggle_value else 'bpm'
    return create_route_hr_map(df, mode=mode, height=500)


# =============================================================================
# Callback 3: Hover auf Elevation-Plot → Marker auf Map aktualisieren
# =============================================================================
@app.callback(
    Output('fig-map', 'figure'),
    Input('fig-elevation', 'hoverData'),
    State('fig-map', 'figure'),
    State('stored-df', 'data'),
    prevent_initial_call=True
)
def highlight_map(hoverData, fig_map, json_data):
    df = pd.read_json(io.StringIO(json_data), orient='split')

    if hoverData is not None:
        point_index = hoverData['points'][0]['pointIndex']
        lat, lon = df.iloc[point_index][['lat', 'lon']]
        fig_map['data'][-1]['lat'] = [lat]
        fig_map['data'][-1]['lon'] = [lon]

    return fig_map


# =============================================================================
# Callback 4: Pixel-Heatmap (Matplotlib PNG) mit Stadt-Toggle
# =============================================================================
@app.callback(
    Output('fig-pixel-heatmap', 'figure'),
    Output('heatmap-city-info', 'children'),
    Input('file-dropdown', 'value'),
    Input('heatmap-mode-toggle', 'value'),
    # prevent_initial_call=True  ← bewusst deaktiviert: initiales Laden beim App-Start
)
def update_pixel_heatmap(selected_file, toggle_value):
    """
    Rendert die Pixel-Heatmap abhängig vom Toggle-Switch.
    toggle_value == []       → Einzellauf (nur gewählte Datei, count aus Region)
    toggle_value == ['city'] → Region (alle Läufe mit gleichem Stadtcode)
    """
    if not selected_file or selected_file in ['NO_FILES', 'NO_FOLDER', 'ERROR', None]:
        empty = go.Figure()
        empty.update_layout(
            paper_bgcolor='#1e1e1e',
            font=dict(color='white'),
            title=dict(text='Datei wählen...', font=dict(color='white'))
        )
        return empty, ''

    city_code      = extract_city_code(selected_file)
    city_info_text = f'Erkannte Region: {city_code}' if city_code else 'Kein Stadtcode erkannt'

    # Immer alle Stadt-Läufe laden (für korrekten Count-Grid-Kontext)
    all_options = list_files()
    if city_code:
        city_dfs, _ = load_runs_for_city(city_code, all_options)
    else:
        city_dfs = []

    # Fallback: keine Stadt-Läufe → nur aktuelle Datei
    if not city_dfs:
        try:
            city_dfs = [process_selected_file(selected_file)]
        except Exception:
            city_dfs = []

    if not toggle_value or 'city' not in toggle_value:
        # --- Einzellauf-Modus ---
        # count_grid aus allen Läufen der Region, aber nur dieser Lauf gezeichnet
        try:
            df_single = process_selected_file(selected_file)
        except Exception:
            df_single = pd.DataFrame()

        fig = create_pixel_heatmap(
            dataframes=city_dfs,
            highlight_df=df_single,
            mode='single',
            city_code=city_code,
            n_city_runs=len(city_dfs),
            img_width=800, img_height=500,
        )
        city_info_text = f'Region {city_code} · Einzellauf · Count aus {len(city_dfs)} Läufen'

    else:
        # --- Region-Modus: alle Läufe der Stadt anzeigen ---
        fig = create_pixel_heatmap(
            dataframes=city_dfs,
            mode='city',
            city_code=city_code,
            n_city_runs=len(city_dfs),
            img_width=800, img_height=500,
        )
        city_info_text = f'Region {city_code} · {len(city_dfs)} Läufe geladen'

    return fig, city_info_text




# Callback 5: Export SVG
@app.callback(
    Output('export-status', 'children'),
    Input('export-button', 'n_clicks'),
    State('stored-df', 'data'),
    State('file-dropdown', 'value'),
    prevent_initial_call=True
)
def export_summary_image(n_clicks, json_data, selected_file):
    if n_clicks and json_data and selected_file:
        try:
            print(f"Export wurde geklickt für Datei: {selected_file}")

            # DataFrame aus bereits geladenen Daten erstellen
            df = pd.read_json(io.StringIO(json_data), orient='split')

            if df.empty:
                return html.Div("Export fehlgeschlagen: Keine Daten verfügbar",
                              style={'color': 'red', 'fontSize': '12px'})

            # Statistiken berechnen (gleich wie im Info-Banner)
            total_distance_km = df['cum_dist_km'].iloc[-1] if 'cum_dist_km' in df.columns else 0
            total_time_str = df['duration_hms'].iloc[-1] if 'duration_hms' in df.columns else "00:00:00"
            total_seconds = df['time_diff_sec'].iloc[-1] if 'time_diff_sec' in df.columns else 0

            # Pace berechnen
            avg_pace = calculate_pace(total_distance_km, total_seconds)

            # Output-Dateiname
            base_name = os.path.splitext(os.path.basename(selected_file))[0]
            output_filename = f"{base_name}_overlay.svg"

            print(f"Stats - Distance: {total_distance_km:.1f}km, Time: {total_time_str}, Pace: {avg_pace}")

            # SVG erstellen
            svg = create_strava_style_svg(
                df=df,
                total_distance_km=total_distance_km,
                total_time=total_time_str,
                avg_pace=avg_pace,
                width=800,
                height=600
            )

            # SVG speichern
            save_svg(svg, output_filename)

            return html.Div(
                f"Export erfolgreich! Datei: {output_filename}",
                style={'color': 'green', 'fontSize': '12px', 'marginTop': '5px'}
            )

        except Exception as e:
            print(f"Export-Fehler: {str(e)}")
            return html.Div(
                f"Export fehlgeschlagen: {str(e)}",
                style={'color': 'red', 'fontSize': '12px', 'marginTop': '5px'}
            )

    return ""


# === Run Server ===
if __name__ == '__main__':
    app.run(debug=True,
            port=8051,
            threaded=True,
            processes=1
    )

    
# NOTE:
#    Zusammenhang zwischen Pace und Geschwindigkeit
#     - Pace = Minuten pro Kilometer (z. B. 5:40/km)
#     - Geschwindigkeit = Kilometer pro Stunde (z. B. 10.71 km/h)
# 
