Ugrás a fő tartalomra

Bitfaragó a nyugdíj kapujában

 

Bitfaragó a nyugdíj kapujában: Mire jó az AI, ha nem receptet akarsz generálni?

Ha a mainstream médiát hallgatjuk, a mesterséges intelligencia arra való, hogy tízévesek rajzfilmes avatárt csináljanak magukból, vagy irodisták kérjenek tőle udvarias elutasító levelet a főnöküknek.

Egy évtizedekig terminálokon, relációs adatbázisokon, műszerfalakon és forrasztópákán edződött műszaki agynak viszont az AI nem csodafegyver és nem játék, hanem egy végtelen türelmű junior fejlesztő, digitális laborasszisztens és fordított mérnöki segédeszköz.

Nézzük meg pontról pontra, hogyan állítható hadrendbe a technológia egy tapasztalt, kódoló és túrázó műszaki ember műhelyében.

1. Kódolás: Python és Android fejlesztés turbófokozaton

Nem kell mindent a nulláról legépelni, és a dokumentációk böngészése sem viszi el a fél napot:

  • Gyors refaktorálás és boilerplate: Pár másodperc alatt megírja a Room adatbázis migrációs szkriptjét Kotlinban, vagy összeállítja a Pandas/GeoPandas pipeline-t a túraadatok feldolgozásához.

  • Architektúra-ellenőrzés: Átnyújtható neki egy kusza Python modul azzal a felütéssel: „Keresd meg a memóriaszivárgást és tedd szálbiztossá az adatgyűjtést.”

  • API wrapper generálás: Pillanatok alatt készít REST végpontokhoz vagy helyi IoT hardverekhez tiszta, típusos klienskönyvtárat.

2. Térképek, túrázás és kerékpározás: Térinformatikai barkácsolás

A digitális térképészet és a terepi mozgás az adatelemzés aranybányája:

  • GPX adatelemzés saját kóddal: A rögzített GPX trackekből a Python szkriptekkel kinyert szintkülönbségek, pulzus- és sebességadatok elemzéséhez az AI megírja a simító algoritmusokat és a kalória- vagy wattbecslést.

  • OpenStreetMap automatizáció: GeoJSON adatok szűrése, Overpass API lekérdezések összeállítása (pl. „Minden ivóvízvételi pont és pihenőhely a tervezett 40 km-es bakonyi szakasz mentén”).

  • Időjárás és magassági profil modellezés: Olyan mikroszkriptek építése, amelyek az útvonal mentén elhelyezett magassági pontokhoz igazítják a becsült menetidőt a szintemelkedés és az uralkodó szélirány alapján.

3. Fényképezés és videókészítés: Raw adatoktól a vágóasztalig

A vizuális műfajban a neurális hálók ma már a hardver szintjén dolgoznak:

  • Zajos, régi felvételek restaurálása: Helyi vagy felhős modellekkel zajszűrés, élesítés, felbontásnövelés (upscaling) anélkül, hogy a részletek műanyaggá válnának.

  • Automatikus indexelés és metaadat-katalogizálás: Python segítségével helyi képfelismerő modellekkel kulcsszavazható több ezer archív családi és természetfotó (EXIF adatok, helyszínek, növényfajok szerint).

  • Videó utómunka gyorsítása: FFmpeg parancssori szörnyetegek összerakása egyetlen prompttal, hangzajok eltávolítása neurális szűrőkkel, automatikus feliratozás és jelenetvágási pontok detektálása.

4. Fizikai barkácsolás: Digitális tervasztal és hibakeresés

A fizikai világban a multimodális (látó) AI működik a leghatékonyabban:

  • Szenzorkódok és mikrokontrollerek: Legyen szó ESP32-ről vagy Raspberry Piről, a hardveres lábkiosztások, I2C busz konfigurációk és szenzoros adatkiolvasások percek alatt működő kódba önthetők.

  • Alkatrész-azonosítás fotó alapján: Ráfotózva egy lekopott feliratú IC-re vagy furcsa mechanikai alkatrészre, az AI segít kideríteni a lábkiosztást, helyettesítő típust vagy a menettípust.

  • 3D modellezés és nyomtatás: OpenSCAD programkódok generálása egyszerűbb egyedi rögzítőkhöz, biciklis kormányadapterekhez vagy szenzordobozokhoz közvetlenül szöveges leírásból.

A lényeg: Irányító maradsz, nem felhasználó

A kulcs a perspektíva: az AI nem helyettesíti a 40 év alatt felhalmozott műszaki intuíciót és fizikai tapasztalatot. Viszont leveszi a válladról a szintaktikai bíbelődést, felgyorsítja a prototípusok építését, és közvetlen hidat képez a monitoron futó kód és a bringára szerelt hardver között.


Egy példa


A nyers GPS-adatok tipikus hibája a vertikális pontatlanság: a másodpercenként rögzített magasság ugrál, ami szűretlenül akár 30–50%-kal is túlbecsülheti a valós szintemelkedést.

Az alábbi önálló szkript gpxpy segítségével dolgozza fel a fájlt, a WGS84 koordináták távolságát haversine-képlettel számítja, a vertikális mérési zajt pedig Savitzky–Golay digitális szűrővel vagy mozgóátlaggal simítja.

Szükséges csomagok telepítése

Bash
pip install gpxpy numpy pandas scipy matplotlib

Python feldolgozó szkript (gpx_analyzer.py)

Python
import math
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.signal import savgol_filter
import gpxpy


def haversine_distance(lat1, lon1, lat2, lon2):
    """Két WGS84 koordináta közötti távolság méterben."""
    r_earth = 6371000.0  # Föld sugara méterben
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)

    a = (
        math.sin(delta_phi / 2.0) ** 2
        + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2.0) ** 2
    )
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return r_earth * c


def load_gpx_to_dataframe(gpx_path):
    """GPX trackpontok kinyerése DataFrame struktúrába."""
    with open(gpx_path, "r", encoding="utf-8") as f:
        gpx = gpxpy.parse(f)

    records = []
    for track in gpx.tracks:
        for segment in track.segments:
            for pt in segment.points:
                records.append(
                    {
                        "lat": pt.latitude,
                        "lon": pt.longitude,
                        "ele": pt.elevation,
                        "time": pt.time,
                    }
                )

    df = pd.DataFrame(records)
    if df.empty:
        raise ValueError("A GPX-fájl nem tartalmaz érvényes trackpontokat.")
    return df


def process_gpx(df, window_length=15, polyorder=2):
    """Kumulatív távolság számítása, magasság simítása és statisztikák."""
    # 1. Távolságok számítása lépésenként
    distances = [0.0]
    for i in range(1, len(df)):
        d = haversine_distance(
            df.at[i - 1, "lat"],
            df.at[i - 1, "lon"],
            df.at[i, "lat"],
            df.at[i, "lon"],
        )
        distances.append(d)

    df["step_dist"] = distances
    df["cum_dist_km"] = np.cumsum(distances) / 1000.0

    # 2. Magassági adatok simítása (Savitzky-Golay szűrő zajszűréshez)
    # Ha a mintaszám kisebb, mint az ablakméret, dinamikusan csökkentjük
    actual_window = min(window_length, len(df))
    if actual_window % 2 == 0:
        actual_window -= 1

    if actual_window > polyorder:
        df["ele_smooth"] = savgol_filter(df["ele"], actual_window, polyorder)
    else:
        df["ele_smooth"] = df["ele"].rolling(window=3, min_periods=1).mean()

    # 3. Szintkülönbségek számítása a simított görbén
    ele_diffs = np.diff(df["ele_smooth"], prepend=df["ele_smooth"].iloc[0])
    
    # Hozzáadunk egy minimális küszöböt (pl. 0.2 m), hogy az apró remegést kizárjuk
    ele_diffs_thresholded = np.where(np.abs(ele_diffs) > 0.2, ele_diffs, 0.0)

    total_ascent = float(np.sum(ele_diffs_thresholded[ele_diffs_thresholded > 0]))
    total_descent = float(np.abs(np.sum(ele_diffs_thresholded[ele_diffs_thresholded < 0])))

    return df, total_ascent, total_descent


def plot_profile(df, total_ascent, total_descent, output_png=None):
    """Magassági profil megjelenítése a nyers és a simított adatokkal."""
    plt.figure(figsize=(12, 6))

    # Nyers adat háttérben
    plt.plot(
        df["cum_dist_km"],
        df["ele"],
        label="Nyers GPS adat",
        color="#b0bec5",
        linestyle="--",
        linewidth=1,
        alpha=0.7,
    )

    # Simított profil
    plt.plot(
        df["cum_dist_km"],
        df["ele_smooth"],
        label="Simított profil (Savitzky-Golay)",
        color="#0288d1",
        linewidth=2,
    )
    plt.fill_between(
        df["cum_dist_km"],
        df["ele_smooth"],
        df["ele_smooth"].min() - 10,
        color="#b3e5fc",
        alpha=0.3,
    )

    total_dist = df["cum_dist_km"].iloc[-1]
    title_text = (
        f"Túra profilja\n"
        f"Össztáv: {total_dist:.2f} km | "
        f"Összes szintemelkedés: +{total_ascent:.0f} m | "
        f"Összes lejtés: -{total_descent:.0f} m"
    )

    plt.title(title_text, fontsize=13, fontweight="bold", pad=12)
    plt.xlabel("Megtett távolság (km)", fontsize=11)
    plt.ylabel("Tengerszint feletti magasság (m)", fontsize=11)
    plt.grid(True, linestyle=":", alpha=0.6)
    plt.legend(loc="upper right")
    plt.tight_layout()

    if output_png:
        plt.savefig(output_png, dpi=200)
        print(f"[OK] Diagram mentve: {output_png}")
    plt.show()


if __name__ == "__main__":
    GPX_FILE = "turautes.gpx"  # Cseréld ki a saját GPX fájlod elérési útjára

    try:
        data = load_gpx_to_dataframe(GPX_FILE)
        processed_data, ascent, descent = process_gpx(data, window_length=21)

        print(f"--- Statisztika ---")
        print(f"Trackpontok száma: {len(processed_data)}")
        print(f"Teljes táv:        {processed_data['cum_dist_km'].iloc[-1]:.2f} km")
        print(f"Szintemelkedés:    +{ascent:.1f} m")
        print(f"Szintesés:         -{descent:.1f} m")
        print(f"Min. magasság:     {processed_data['ele_smooth'].min():.1f} m")
        print(f"Max. magasság:     {processed_data['ele_smooth'].max():.1f} m")

        plot_profile(processed_data, ascent, descent, output_png="magassagi_profil.png")

    except FileNotFoundError:
        print(f"[HIBA] A fájl nem található: {GPX_FILE}")

Működési elv és finomhangolás

  1. WGS84 projekció: A haversine_distance függvény gömbi trigonometriával számítja ki a szomszédos pontok távolságát, így nem szükséges külső térinformatikai vetületet transzformálni.

  2. Savitzky–Golay szűrő: Mozgó ablakban lokális másodfokú polinomot illeszt a magassági adatokra. Előnye a sima mozgóátlaggal szemben, hogy a hirtelen gerinceket, csúcsokat nem „vágja le”, de a GPS másodperces remegését kiveszi.

  3. Zajküszöb (0.2 m): A GPS minimális driftjét az integrálás előtt egy alsó határérték nullázza, megelőzve az álló helyzetben halmozódó szintemelkedést.



Finomítás


A GPS rögzítés során álló helyzetben a koordináták driftelése hamis mozgást és sebességet generál. A tisztított mozgási idő és a megállások szétválasztásához egy sebesség-küszöbértéket (SPEED_THRESHOLD = 0.8 m/s ≈ 2.88 km/h) és egy minimális megállási időtartamot (MIN_STOP_DURATION = 30 s) alkalmazunk.

Bővített Python szkript (gpx_time_analyzer.py)

Python
import math
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.signal import savgol_filter
import gpxpy


def haversine_distance(lat1, lon1, lat2, lon2):
    """Két WGS84 koordináta közötti távolság méterben."""
    r_earth = 6371000.0
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)

    a = (
        math.sin(delta_phi / 2.0) ** 2
        + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2.0) ** 2
    )
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return r_earth * c


def format_duration(seconds):
    """Másodpercek átalakítása ÓÓ:PP:MP formátumra."""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = int(seconds % 60)
    return f"{hours:02d}:{minutes:02d}:{secs:02d}"


def load_gpx_to_dataframe(gpx_path):
    """GPX trackpontok kinyerése időbélyegekkel."""
    with open(gpx_path, "r", encoding="utf-8") as f:
        gpx = gpxpy.parse(f)

    records = []
    for track in gpx.tracks:
        for segment in track.segments:
            for pt in segment.points:
                records.append(
                    {
                        "lat": pt.latitude,
                        "lon": pt.longitude,
                        "ele": pt.elevation,
                        "time": pt.time,
                    }
                )

    df = pd.DataFrame(records)
    if df.empty:
        raise ValueError("A GPX-fájl nem tartalmaz trackpontokat.")
    if df["time"].isnull().all():
        raise ValueError("A GPX-fájl nem tartalmaz időbélyegeket.")
    
    df["time"] = pd.to_datetime(df["time"])
    return df


def analyze_gpx(
    df,
    window_length=21,
    polyorder=2,
    speed_threshold_kmh=2.5,
    min_stop_seconds=45,
):
    """Teljes metrikai elemzés távolságra, szintre, időre és megállásokra."""
    n_points = len(df)
    speed_threshold_ms = speed_threshold_kmh / 3.6

    # 1. Delta távolságok és idők kiszámítása
    step_dists = [0.0]
    step_times = [0.0]

    for i in range(1, n_points):
        d = haversine_distance(
            df.at[i - 1, "lat"], df.at[i - 1, "lon"],
            df.at[i, "lat"], df.at[i, "lon"]
        )
        dt = (df.at[i, "time"] - df.at[i - 1, "time"]).total_seconds()
        step_dists.append(d)
        step_times.append(max(0.0, dt))

    df["step_dist"] = step_dists
    df["step_time_s"] = step_times
    df["cum_dist_km"] = np.cumsum(step_dists) / 1000.0

    # 2. Pillanatnyi sebesség (m/s és km/h) szűréssel
    speeds_ms = [0.0]
    for d, t in zip(step_dists[1:], step_times[1:]):
        speeds_ms.append(d / t if t > 0 else 0.0)
    df["speed_ms"] = speeds_ms
    df["speed_kmh"] = df["speed_ms"] * 3.6

    # 3. Magasság simítása és szintkülönbségek
    actual_window = min(window_length, n_points)
    if actual_window % 2 == 0:
        actual_window -= 1

    if actual_window > polyorder:
        df["ele_smooth"] = savgol_filter(df["ele"], actual_window, polyorder)
    else:
        df["ele_smooth"] = df["ele"].rolling(window=3, min_periods=1).mean()

    ele_diffs = np.diff(df["ele_smooth"], prepend=df["ele_smooth"].iloc[0])
    ele_diffs_thresh = np.where(np.abs(ele_diffs) > 0.2, ele_diffs, 0.0)
    total_ascent = float(np.sum(ele_diffs_thresh[ele_diffs_thresh > 0]))
    total_descent = float(np.abs(np.sum(ele_diffs_thresh[ele_diffs_thresh < 0])))

    # 4. Megállások és mozgás detektálása
    df["is_moving"] = df["speed_ms"] >= speed_threshold_ms

    stops = []
    current_stop_start = None
    current_stop_duration = 0.0
    current_stop_points = []

    for idx, row in df.iterrows():
        if not row["is_moving"]:
            if current_stop_start is None:
                current_stop_start = row["time"]
            current_stop_duration += row["step_time_s"]
            current_stop_points.append(row)
        else:
            if current_stop_start is not None:
                if current_stop_duration >= min_stop_seconds:
                    stops.append({
                        "start_time": current_stop_start,
                        "duration_s": current_stop_duration,
                        "lat": np.mean([p["lat"] for p in current_stop_points]),
                        "lon": np.mean([p["lon"] for p in current_stop_points]),
                        "km_mark": current_stop_points[0]["cum_dist_km"],
                    })
                current_stop_start = None
                current_stop_duration = 0.0
                current_stop_points = []

    # Utolsó megállás lezárása
    if current_stop_start and current_stop_duration >= min_stop_seconds:
        stops.append({
            "start_time": current_stop_start,
            "duration_s": current_stop_duration,
            "lat": np.mean([p["lat"] for p in current_stop_points]),
            "lon": np.mean([p["lon"] for p in current_stop_points]),
            "km_mark": current_stop_points[0]["cum_dist_km"],
        })

    # 5. Idő- és sebességstatisztikák összesítése
    total_time_s = (df["time"].iloc[-1] - df["time"].iloc[0]).total_seconds()
    moving_time_s = df.loc[df["is_moving"], "step_time_s"].sum()
    paused_time_s = max(0.0, total_time_s - moving_time_s)

    total_dist_km = df["cum_dist_km"].iloc[-1]
    moving_dist_km = df.loc[df["is_moving"], "step_dist"].sum() / 1000.0

    avg_speed_total_kmh = (total_dist_km / (total_time_s / 3600.0)) if total_time_s > 0 else 0.0
    avg_speed_moving_kmh = (moving_dist_km / (moving_time_s / 3600.0)) if moving_time_s > 0 else 0.0
    max_speed_kmh = df["speed_kmh"].quantile(0.99)  # 99-es percentilis a GPS mérési tüskék ellen

    summary = {
        "total_distance_km": total_dist_km,
        "total_ascent_m": total_ascent,
        "total_descent_m": total_descent,
        "total_time_s": total_time_s,
        "moving_time_s": moving_time_s,
        "paused_time_s": paused_time_s,
        "avg_speed_total_kmh": avg_speed_total_kmh,
        "avg_speed_moving_kmh": avg_speed_moving_kmh,
        "max_speed_kmh": max_speed_kmh,
        "stops": stops,
    }

    return df, summary


def print_summary(s):
    """Eredmények kiírása formázva."""
    print("=" * 50)
    print("           TÚRA / EDZÉS STATISZTIKA")
    print("=" * 50)
    print(f"Teljes távolság:      {s['total_distance_km']:.2f} km")
    print(f"Szintemelkedés (+):    {s['total_ascent_m']:.0f} m")
    print(f"Szintesés (-):         {s['total_descent_m']:.0f} m")
    print("-" * 50)
    print(f"Teljes eltelt idő:     {format_duration(s['total_time_s'])}")
    print(f"Tiszta mozgási idő:    {format_duration(s['moving_time_s'])}")
    print(f"Pihenőidő (állás):     {format_duration(s['paused_time_s'])}")
    print("-" * 50)
    print(f"Bruttó átlagsebesség:  {s['avg_speed_total_kmh']:.2f} km/h")
    print(f"Mozgási átlagsebesség: {s['avg_speed_moving_kmh']:.2f} km/h")
    print(f"Max. sebesség (szűrt): {s['max_speed_kmh']:.2f} km/h")
    print("-" * 50)
    print(f"Detektált megállások száma: {len(s['stops'])}")
    for idx, stop in enumerate(s['stops'], 1):
        print(f"  #{idx} | {stop['km_mark']:.2f}. km-nél | "
              f"Időtartam: {format_duration(stop['duration_s'])} | "
              f"Kezdés: {stop['start_time'].strftime('%H:%M:%S')}")
    print("=" * 50)


if __name__ == "__main__":
    GPX_FILE = "turautes.gpx"

    try:
        raw_df = load_gpx_to_dataframe(GPX_FILE)
        df_processed, stats = analyze_gpx(
            raw_df,
            speed_threshold_kmh=2.8,  # Gyaloglás alatti sebesség = állás
            min_stop_seconds=60       # Legalább 1 perces egy helyben maradás = megállás
        )
        print_summary(stats)

    except FileNotFoundError:
        print(f"[HIBA] Fájl nem található: {GPX_FILE}")

Működési logika és szűrési mechanizmusok

  • Pontatlan csúcssebességek levágása: A kód quantile(0.99) értékkel határozza meg a maximális sebességet. Ezzel kizárja azokat a hibás 1 másodperces kiugrásokat, amikor a GPS ugrása miatt a rendszer 80–100 km/h-s pillanatnyi sebességet számolna.

  • Álló helyzeti pontszórás eliminálása: A speed_threshold_kmh alatti elmozdulásokat a szkript nem adja hozzá a tiszta mozgási időhöz, így a padon ülve vagy kilátóponton rögzített „remegés” nem növeli a mozgási időt és nem rontja a mozgási átlagsebességet.

  • Klaszterezett megállás-detektálás: A megállási események átlagolt GPS-koordinátát és kilométer-szelvényt kapnak, így közvetlenül ráilleszthetők térképre vagy POI adatbázisra.


Az alábbi kiegészítő modul a korábban feldolgozott DataFrame adatait használja fel. Két közös X-tengelyű (cum_dist_km) subplotot készít: a felső a magassági profilt, az alsó a pillanatnyi és átlagsebességet jeleníti meg, a detektált pihenőhelyeket pedig függőleges sávként mindkét diagramon átvezeti.

Diagramrajzoló modul (gpx_plotter.py)

Python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


def plot_dual_profile(df, stats, output_png=None):
    """
    Két egymás alatti subplot rajzolása:
    1. Magassági profil (szintemelkedés, szintcsökkenés)
    2. Sebességprofil (pillanatnyi és mozgási átlagsebesség)
    Közös X-tengellyel és kiemelt megállóhely-sávokkal.
    """
    fig, (ax_ele, ax_spd) = plt.subplots(
        nrows=2, ncols=1, figsize=(13, 8), sharex=True,
        gridspec_kw={"height_ratios": [1.2, 1.0]}
    )

    x = df["cum_dist_km"]

    # --- 1. FELSŐ SUBPLOT: Magassági profil ---
    ax_ele.plot(
        x, df["ele_smooth"],
        color="#1565c0", linewidth=2.0, label="Tengerszint feletti magasság"
    )
    min_ele = df["ele_smooth"].min()
    ax_ele.fill_between(
        x, df["ele_smooth"], min_ele - 5,
        color="#bbdefb", alpha=0.45
    )

    ax_ele.set_ylabel("Magasság (m)", fontsize=11, fontweight="bold")
    ax_ele.set_ylim(bottom=min_ele - 10)
    ax_ele.grid(True, linestyle=":", alpha=0.6)
    ax_ele.legend(loc="upper right", frameon=True)

    header_text = (
        f"Túra összegzés: {stats['total_distance_km']:.2f} km | "
        f"+{stats['total_ascent_m']:.0f} m / -{stats['total_descent_m']:.0f} m | "
        f"Mozgási idő: {int(stats['moving_time_s'] // 3600):02d}:{int((stats['moving_time_s'] % 3600) // 60):02d}"
    )
    ax_ele.set_title(header_text, fontsize=12, fontweight="bold", pad=12)

    # --- 2. ALSÓ SUBPLOT: Sebességprofil ---
    # Nyers sebesség zaja helyett 5 pontos gördülő átlag a tiszta ábrázoláshoz
    speed_smooth = df["speed_kmh"].rolling(window=5, min_periods=1, center=True).mean()

    ax_spd.plot(
        x, speed_smooth,
        color="#2e7d32", linewidth=1.8, label="Pillanatnyi sebesség (simított)"
    )
    ax_spd.axhline(
        stats["avg_speed_moving_kmh"],
        color="#d32f2f", linestyle="--", linewidth=1.5,
        label=f"Mozgási átlag ({stats['avg_speed_moving_kmh']:.1f} km/h)"
    )

    ax_spd.set_xlabel("Megtett távolság (km)", fontsize=11, fontweight="bold")
    ax_spd.set_ylabel("Sebesség (km/h)", fontsize=11, fontweight="bold")
    ax_spd.set_ylim(bottom=0, top=max(stats["max_speed_kmh"] * 1.15, 10))
    ax_spd.grid(True, linestyle=":", alpha=0.6)
    ax_spd.legend(loc="upper right", frameon=True)

    # --- 3. MEGÁLLÓK JELÖLÉSE MINDKÉT SUBPLOTON ---
    for i, stop in enumerate(stats.get("stops", [])):
        km = stop["km_mark"]
        dur_min = stop["duration_s"] / 60.0

        # Függőleges sáv mindkét tengelyen
        for ax in (ax_ele, ax_spd):
            ax.axvline(km, color="#ff8f00", linestyle="-.", alpha=0.75, linewidth=1.3)

        # Felirat a felső diagram tetején
        ax_ele.text(
            km, ax_ele.get_ylim()[1] * 0.98,
            f" M{i+1}\n({dur_min:.0f}p)",
            color="#e65100", fontsize=8.5, fontweight="bold",
            ha="center", va="top"
        )

    plt.tight_layout()

    if output_png:
        plt.savefig(output_png, dpi=220)
        print(f"[OK] Kétpaneles diagram mentve: {output_png}")

    plt.show()


# --- Használati példa a korábbi feldolgozóval együtt ---
if __name__ == "__main__":
    from gpx_time_analyzer import load_gpx_to_dataframe, analyze_gpx

    GPX_PATH = "turautes.gpx"

    try:
        raw_df = load_gpx_to_dataframe(GPX_PATH)
        df_proc, summary_stats = analyze_gpx(raw_df)

        plot_dual_profile(
            df_proc,
            summary_stats,
            output_png="magassag_es_sebesseg_profil.png"
        )
    except FileNotFoundError:
        print(f"[HIBA] Nem található fájl: {GPX_PATH}")

Megjelenítési sajátosságok

  • Csatolt X-tengely (sharex=True): Ha a Matplotlib ablakban zoomolsz vagy mozgatod a távolsági tengelyt, a két diagram szinkronban marad.

  • Aszimmetrikus rácsarány (height_ratios=[1.2, 1.0]): A szintrajz kapja a nagyobb vertikális hangsúlyt, mivel a terep vonalvezetése jobban igényli a felbontást.

  • Gördülő sebességsimítás: A rajzolás pillanatában futtatott 5 pontos rolling.mean() kisimítja a másodperces GPS ugrásokat, de nem torzítja el a hosszabb gyorsulásokat vagy lejtőzéseket.

  • Megállási jelölők (M1, M2...): A narancssárga pont-vonal sávok közvetlenül összekötik az álló helyzetet a szintprofil megfelelő koordinátájával (pl. hegytetőn tartott pihenő).


Az alábbi modul a korábban kinyert adatstruktúrából önálló, böngészőben megnyitható HTML fájlt generál a Plotly segítségével. A két subplot (magasság és sebesség) közös X tengelyt használ, a hovermode="x unified" beállításnak köszönhetően pedig az egér egyetlen függőleges vonalán egyszerre látható az adott kilométernél rögzített pontos időpont, magasság, sebesség és állapot.

Csomag telepítése

Bash
pip install plotly

Plotly interaktív generátor (gpx_plotly_generator.py)

Python
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots


def generate_interactive_html(df, stats, output_html="tura_profil.html"):
    """Önálló interaktív Plotly HTML generálása szinkronizált kurzorral és megállókkal."""
    # 1. Előkészítés: időbélyeg formázás és mozgóátlag sebesség
    df = df.copy()
    df["time_str"] = df["time"].dt.strftime("%H:%M:%S")
    df["speed_smooth"] = (
        df["speed_kmh"].rolling(window=5, min_periods=1, center=True).mean()
    )

    # 2. Két összefűzött subplot létrehozása közös X tengellyel
    fig = make_subplots(
        rows=2,
        cols=1,
        shared_xaxes=True,
        vertical_spacing=0.08,
        row_heights=[0.58, 0.42],
        subplot_titles=(
            "<b>Magassági profil</b>",
            "<b>Sebesség és megállások</b>",
        ),
    )

    # --- 1. SUBPLOT: Magasság ---
    fig.add_trace(
        go.Scatter(
            x=df["cum_dist_km"],
            y=df["ele_smooth"],
            mode="lines",
            name="Magasság",
            line=dict(color="#1976d2", width=2.5),
            fill="tozeroy",
            fillcolor="rgba(25, 118, 210, 0.15)",
            customdata=df[["time_str", "speed_smooth", "ele"]],
            hovertemplate=(
                "<b>Magasság:</b> %{y:.1f} m (nyers: %{customdata[2]:.1f} m)<br>"
                "<b>Időpont:</b> %{customdata[0]}<extra></extra>"
            ),
        ),
        row=1,
        col=1,
    )

    # --- 2. SUBPLOT: Sebesség ---
    fig.add_trace(
        go.Scatter(
            x=df["cum_dist_km"],
            y=df["speed_smooth"],
            mode="lines",
            name="Pillanatnyi sebesség",
            line=dict(color="#2e7d32", width=2),
            hovertemplate="<b>Sebesség:</b> %{y:.1f} km/h<extra></extra>",
        ),
        row=2,
        col=1,
    )

    # Mozgási átlagsebesség referenciavonal
    fig.add_trace(
        go.Scatter(
            x=[df["cum_dist_km"].iloc[0], df["cum_dist_km"].iloc[-1]],
            y=[stats["avg_speed_moving_kmh"], stats["avg_speed_moving_kmh"]],
            mode="lines",
            name=f"Mozgási átlag ({stats['avg_speed_moving_kmh']:.1f} km/h)",
            line=dict(color="#d32f2f", width=1.5, dash="dash"),
            hoverinfo="skip",
        ),
        row=2,
        col=1,
    )

    # --- MEGÁLLÓK JELÖLÉSE ÉS FÜGGŐLEGES SÁVOK ---
    for i, stop in enumerate(stats.get("stops", []), 1):
        km = stop["km_mark"]
        dur_min = stop["duration_s"] / 60.0
        start_clk = stop["start_time"].strftime("%H:%M:%S")

        # Függőleges referenciavonal mindkét sávon át
        fig.add_vline(
            x=km,
            line_width=1.5,
            line_dash="dot",
            line_color="#f57c00",
            opacity=0.8,
        )

        # Rögzített megálló pont az alsó sebességdiagramon (0 km/h-nál)
        fig.add_trace(
            go.Scatter(
                x=[km],
                y=[0],
                mode="markers+text",
                name=f"Megálló #{i}",
                marker=dict(size=9, color="#e65100", symbol="triangle-up"),
                text=[f"M{i}"],
                textposition="top center",
                hovertemplate=(
                    f"<b>Megálló #{i}</b><br>"
                    f"Kezdés: {start_clk}<br>"
                    f"Időtartam: {dur_min:.1f} perc<br>"
                    f"Pozíció: {km:.2f} km<extra></extra>"
                ),
                showlegend=False,
            ),
            row=2,
            col=1,
        )

    # --- FEJLÉC ÉS ELRENDEZÉS BEÁLLÍTÁSA ---
    header_title = (
        f"<b>Túra analitika</b> | Táv: <b>{stats['total_distance_km']:.2f} km</b> | "
        f"Szint: <b>+{stats['total_ascent_m']:.0f} m / -{stats['total_descent_m']:.0f} m</b> | "
        f"Mozgási idő: <b>{int(stats['moving_time_s'] // 3600):02d}:{int((stats['moving_time_s'] % 3600) // 60):02d}</b>"
    )

    min_ele = df["ele_smooth"].min()
    fig.update_yaxes(
        title_text="Magasság (m)",
        range=[max(0, min_ele - 20), df["ele_smooth"].max() + 20],
        row=1,
        col=1,
    )
    fig.update_yaxes(
        title_text="Sebesség (km/h)",
        range=[0, max(stats["max_speed_kmh"] * 1.15, 12)],
        row=2,
        col=1,
    )
    fig.update_xaxes(
        title_text="Megtett távolság (km)",
        row=2,
        col=1,
    )

    fig.update_layout(
        title=dict(text=header_title, font=dict(size=15), x=0.03, y=0.98),
        hovermode="x unified",  # Közös vertikális leolvasó vonal az egérmozgatáskor
        template="plotly_white",
        legend=dict(
            orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1
        ),
        margin=dict(l=60, r=30, t=80, b=50),
        height=750,
    )

    # 3. HTML fájlba írás
    fig.write_html(output_html, include_plotlyjs="cdn")
    print(f"[OK] Interaktív HTML sikeresen legenerálva: {output_html}")


# --- Futtatási példa ---
if __name__ == "__main__":
    from gpx_time_analyzer import analyze_gpx, load_gpx_to_dataframe

    GPX_INPUT = "turautes.gpx"

    try:
        raw_df = load_gpx_to_dataframe(GPX_INPUT)
        df_processed, trip_stats = analyze_gpx(raw_df)

        generate_interactive_html(
            df_processed, trip_stats, output_html="magassag_sebesseg_profil.html"
        )
    except FileNotFoundError:
        print(f"[HIBA] Nem található fájl: {GPX_INPUT}")

Fő beállítások működése

  • hovermode="x unified": Nem kell külön az egyes vonalakra célozni az egérrel; a kurzor pozíciójánál megjelenő függőleges segédvonal automatikusan egyetlen közös infóablakba fűzi a magasságot, a másodpercre pontos időt és a sebességet.

  • include_plotlyjs="cdn": A generált .html fájl mérete minimális marad (néhány száz kilobájt), mert a megjelenítő motort a böngésző a Cloudflare CDN-ről tölti be, így azonnal továbbküldhető vagy beágyazható weboldalba.

  • Szinkronizált skálázás: A nagyítás (box zoom / pan) és a tengelymozgatás automatikusan összekapcsolva mozog mindkét panelen a shared_xaxes=True miatt.

Megjegyzések