Push It Teil 2: Höhenkorrektur und Grade Adjusted Pace

März 30, 2026·
David Bösiger
David Bösiger
· 5 Min Lesezeit
blog

In einem früheren Beitrag habe ich beschrieben, wie ich Push It gebaut habe - meine selbst gehostete Fitness-App die mit meiner Bangle.js 2 synchronisiert. Die Grundlagen funktionierten: GPS-Tracking, Herzfrequenz, Splits. Aber die Höhendaten waren komplett falsch, und hügelige Läufe mit flachen zu vergleichen war sinnlos. Hier ist wie ich beides gelöst habe.

Das Höhenproblem

Mein erster Lauf zeigte 37 Meter Höhengewinn. Google Maps sagte 135 Meter. Das Höhenprofil sah korrekt aus - ein stetiger Anstieg von 600m auf über 700m - aber die Gewinn/Verlust-Berechnung war daneben.

GPS-Höhe von der Uhr ist verrauscht. Kleine Schwankungen summieren sich wenn man einfach jede positive Änderung addiert. Ein 2-Meter-Schwellenwert half beim Filtern, aber dann verschwanden graduelle Anstiege. Zu aggressiv.

Ich probierte verschiedene Ansätze:

  • Kein Schwellenwert: 170m Gewinn statt 135m. Jedes GPS-Wackeln zählte als Höhenänderung.
  • 2m Schwellenwert: 37m Gewinn. Graduelle Anstiege verschwanden komplett.
  • 100m Sampling, 0.5m Schwellenwert: 127m. Nah dran aber Auflösung ging verloren.

Das echte Problem war nicht der Algorithmus - es waren die Daten.

DEM-Korrektur: Die Quelle reparieren

GPS-Höhe von einer Uhr ist unzuverlässig. Die Lösung die ernsthafte Lauf-Apps verwenden: Digital Elevation Model (DEM) Daten. Statt der Uhr zu vertrauen, eine topografische Datenbank nach der tatsächlichen Bodenhöhe an jeder GPS-Koordinate befragen.

Ich verwende die Open Topo Data API mit SRTM30m Auflösung:

$response = Http::timeout(30)->get("https://api.opentopodata.org/v1/srtm30m", [
    'locations' => $locations, // "47.123,8.456|47.124,8.457|..."
]);

Die API akzeptiert bis zu 100 Koordinaten pro Anfrage, also sample ich die Route und interpoliere dazwischen:

// Punkte sampeln (max 100 pro API-Aufruf)
$sampleRate = max(1, (int) ceil($totalPoints / 100));
$sampledPoints = $points->filter(fn($p, $i) => $i % $sampleRate === 0);

// Nach DEM-Daten für alle Punkte interpolieren
foreach ($points as $i => $point) {
    if (isset($elevationMap[$i])) {
        $demAltitude = $elevationMap[$i];
    } else {
        // Lineare Interpolation zwischen umliegenden gesampelten Punkten
        $ratio = ($i - $prevIdx) / ($nextIdx - $prevIdx);
        $demAltitude = $elevationMap[$prevIdx] +
            $ratio * ($elevationMap[$nextIdx] - $elevationMap[$prevIdx]);
    }
}

Die DEM-Höhe wird separat von der GPS-Höhe gespeichert, sodass man immer beides hat. Das Höhenprofil und Gewinn/Verlust-Berechnungen bevorzugen DEM wenn verfügbar.

Der Höhen-Algorithmus

Auch mit sauberen DEM-Daten kann man nicht einfach Differenzen summieren. Der Algorithmus:

  1. In 75m-Intervallen sampeln - Nicht jeden GPS-Punkt verwenden. Die Höhe alle 75 Meter zurückgelegter Distanz sampeln. Das filtert Rauschen von Punkten die nahe beieinander liegen.

  2. 5-Punkt gleitender Durchschnitt - Die gesampelten Höhen glätten um verbleibendes Zittern zu entfernen.

  3. 0.3m Schwellenwert - Nur Höhenänderungen grösser als 0.3m zählen. Das eliminiert winzige Schwankungen ohne graduelle Anstiege zu verlieren.

// In festen Distanz-Intervallen sampeln
foreach ($points as $point) {
    if ($point['distance_m'] >= $nextSampleDistance) {
        $samples[] = $point['alt'];
        $nextSampleDistance += 75;
    }
}

// 5-Punkt gleitender Durchschnitt
for ($i = 0; $i < count($samples); $i++) {
    $window = [];
    for ($j = max(0, $i - 2); $j <= min(count($samples) - 1, $i + 2); $j++) {
        $window[] = $samples[$j];
    }
    $smoothed[] = array_sum($window) / count($window);
}

// Gewinn/Verlust mit Schwellenwert berechnen
for ($i = 1; $i < count($smoothed); $i++) {
    $diff = $smoothed[$i] - $smoothed[$i - 1];
    if ($diff > 0.3) $gain += $diff;
    elseif ($diff < -0.3) $loss += abs($diff);
}

Ergebnis: 136m Gewinn vs Google Maps’ 135m. Nah genug.

Grade Adjusted Pace

Mit genauen Höhendaten konnte ich ein weiteres Problem angehen: Läufe auf verschiedenem Terrain vergleichen.

5:30/km bergauf bei 8% Steigung zu laufen ist viel härter als 5:30/km auf flachem Boden. Grade Adjusted Pace (GAP) normalisiert die Pace basierend auf der Steigung und gibt eine äquivalente Flachland-Pace.

Die Formel basiert darauf wie Strava es macht:

private function calculateGap(float $paceSeconds, float $gradePercent): float
{
    if ($gradePercent >= 0) {
        // Bergauf: jedes 1% Steigung addiert ~3.5% Aufwand
        $cost = 1 + ($gradePercent * 0.035);
    } else {
        // Bergab: leichter Vorteil bis -10%, dann abnehmend
        $absGrade = abs($gradePercent);
        if ($absGrade <= 10) {
            $cost = 1 - ($absGrade * 0.015);
        } else {
            $cost = 1 - (10 * 0.015) + (($absGrade - 10) * 0.01);
        }
    }
    return $paceSeconds / $cost;
}

Die Kernaussage: Bergauf laufen kostet ungefähr 3.5% mehr Aufwand pro Prozent Steigung. Ein 10%-Anstieg bei 6:00/km Pace entspricht 4:22/km auf flachem Boden. Bergab ist etwas leichter, aber nicht so sehr wie man denkt - steile Abstiege belasten die Beine.

Die Lauf-Stats-Seite zeigt sowohl die tatsächliche Pace als auch GAP für jeden Kilometer-Split:

KmPaceGAPHöheØ HR
16:125:08+42m148
25:455:31+12m155
34:585:22-38m152

Jetzt sehe ich, dass Km 1 tatsächlich meine härteste Anstrengung war, auch wenn die rohe Pace am langsamsten aussieht.

Gym Progression

Neben den Lauf-Stats habe ich auch ein Gym-Progression-Dashboard hinzugefügt. Das ist eines meiner Lieblingsfeatures, weil ich damit tatsächlich sehen kann, dass ich stärker werde. Ich kann verfolgen, wie sich mein Maximalgewicht über die Zeit für jede Übung erhöht, und Trends beim Gesamtvolumen über Sessions hinweg sehen. Eine Übung auszuwählen und die Linie über Monate steigen zu sehen ist echt motivierend. Es hilft mir auch zu erkennen, wenn ich auf einem Plateau feststecke und härter pushen oder meine Routine ändern muss.

Schuh-Tracking

Während der Arbeit an den Stats habe ich auch Schuh-Tracking hinzugefügt. Laufschuhe nutzen sich ab - die meisten haben eine Lebensdauer von 500-800km. Ich wollte wissen, wann ich meine ersetzen muss.

Das Feature ist einfach:

  • Schuhe mit Foto, Marke und Kaufdatum hinzufügen
  • Jedem Lauf einen Schuh zuweisen
  • Gesamtkilometer werden automatisch berechnet
  • Schuhe als “Retired” markieren wenn sie durch sind

Jede Lauf-Detailseite hat ein Dropdown um den Schuh auszuwählen. Die Schuh-Seite zeigt ein Raster mit Fotos und der gesammelten Distanz für jedes Paar. Nichts Ausgefallenes, aber nützlich um zu wissen wann die Schuhe fällig für Ersatz sind.

Schuh-Tracking in Push It

Was ich gelernt habe

1. Vertraue keiner GPS-Höhe. DEM-Korrektur machte den Unterschied zwischen unbrauchbar und genau. Die Open Topo Data API ist gratis und funktioniert gut.

2. Das Sampling-Intervall ist wichtig. Zu häufiges Sampling verstärkt Rauschen. Zu sparsam verliert Detail. 75m war der Sweet Spot für meine Routen.

3. GAP macht hügelige Läufe vergleichbar. Ohne das waren meine Stats irreführend. Ein “langsamer” hügeliger Lauf erfordert oft mehr Anstrengung als ein “schneller” flacher.


Als Nächstes: HRV-Messung mit der Bangle.js - rohe PPG-Signalverarbeitung, Peak-Detection-Algorithmen und warum ein Brustgurt den Handgelenksensor schlägt.