gpx2web

GPX cycling tools

Maven Central gpx

Installation

The gpx library is published on Maven Central.

Maven

<dependency>
  <groupId>io.github.glandais.gpx2web</groupId>
  <artifactId>gpx</artifactId>
  <version>1.5.2</version>
</dependency>

Gradle

dependencies {
    implementation("io.github.glandais.gpx2web:gpx:1.5.2")
}

The badge above is the source of truth for the current version.

GPX Stuff (gpx module)

Power

References:

Wheel bearings

The wheel bearings power loss is calculated as a quadratic function of speed:

Formula: -speed * (91 + 8.7 * speed) / 1000.0

Implementation: WheelBearingsPowerProvider (io.github.glandais.gpx.virtual.power.rolling:22)

Verification: ✅ Formula matches academic literature for wheel and bearing resistance

Rolling resistance

Rolling resistance accounts for tire deformation and surface friction, adjusted for road grade:

Formula: -cos(atan(grade)) * mass * g * speed * crr

Implementation: RollingResistancePowerProvider (io.github.glandais.gpx.virtual.power.rolling:29)

final double mKg = course.getCyclist().getMKg();     // Total mass (cyclist + bike)
final double crr = course.getBike().getCrr();        // Rolling resistance coefficient
final double grade = location.getGrade();           // Road gradient

double coef = Math.cos(Math.atan(grade));           // Grade adjustment factor
double powerRollingResistance = -coef * mKg * Constants.G * location.getSpeed() * crr;

Parameters:

Verification: ✅ Formula correctly implements standard rolling resistance physics

Gravity

Gravitational power accounts for the energy required to climb (or gained when descending) on inclined surfaces:

Formula: -sin(atan(grade)) * mass * g * speed

Implementation: GravPowerProvider (io.github.glandais.gpx.virtual.power.grav:26)

final double mKg = course.getCyclist().getMKg();     // Total mass (cyclist + bike)
double grade = location.getGrade();                  // Road gradient (rise/run)
double coef = Math.sin(Math.atan(grade));           // Grade component factor
double powerGravity = -mKg * Constants.G * location.getSpeed() * coef;

Parameters:

Verification: ✅ Formula correctly implements standard gravitational physics for inclines

Aerodynamic drag

Aerodynamic power represents the largest resistance component at higher cycling speeds, following a cubic relationship with speed:

Part 1: No Wind Conditions

Formula: -aeroCoef * speed³

Implementation: AeroPowerProvider (io.github.glandais.gpx.virtual.power.aero:29)

if (wind.windSpeed() == 0) {
    double speed = location.getSpeed();
    p_air = -aeroCoef * speed * speed * speed;
}

Aerodynamic coefficient calculation:

// From AeroProviderConstant
aeroCoef = cyclist.getCd() * cyclist.getA() * course.getRho() / 2

Parameters:

Verification: ✅ Standard aerodynamic drag formula P = 0.5 × ρ × Cd × A × v³

Part 2: With Wind Conditions

Formula: Complex wind-adjusted calculation using relative velocity

Implementation: AeroPowerProvider.computePAirWithWind() (io.github.glandais.gpx.virtual.power.aero:61)

double alpha = windDirectionAsBearing - bearing;           // Wind angle relative to travel direction
double l1 = speed + windSpeed * Math.cos(alpha);          // Relative velocity component
double l2 = Math.pow(l1, 2);                             // Squared relative velocity
double l3 = speed * speed + windSpeed * windSpeed +       // Total relative velocity squared
           2 * speed * windSpeed * Math.cos(alpha);
double l4 = l2 / l3;                                      // Velocity ratio
double mu = 1.2;                                          // Correction factor
double lambda = l4 + mu * (1 - l4);                      // Combined correction factor

return -aeroCoef * lambda * Math.sqrt(l3) * l1 * speed;

Wind parameters:

Verification: ✅ Formula validated against Isvan (2011) “Power Management for Lightweight Vehicles”

Academic Source: The wind formula implements Equations 21, 22, 24, and 28 from:

Mathematical Derivation:

  1. Apparent wind calculation (Equations 21-22):
    • l3 = u² + v² + 2uvcos(α) → apparent wind speed squared: w² = u² + v² + 2uvcos(α)
    • l1 = u + vcos(α) → component: u + vcos(α)
  2. Aerodynamic aspect ratio correction (Equation 24):
    • l4 = l2/l3 = (u + vcos(α))²/(u² + v² + 2uvcos(α))
    • λ = l4 + μ(1-l4) where μ = 1.2 (crosswind to headwind drag ratio for cyclist)
    • This accounts for effective drag area changes with wind angle
  3. Final power calculation (Equation 28 adapted):
    • P = -aeroCoef × λ × √(w²) × (u + vcos(α)) × u
    • P = -aeroCoef × λ × √(l3) × l1 × speed

Physical meaning:

Default Parameters

The system uses academically validated default parameters for virtual cyclist simulation:

Bicycle Parameters (Constants.java)

Parameter Value Unit Description Academic Source
DEFAULT_CRR 0.004 - Rolling resistance coefficient bicyclerollingresistance.com research
DEFAULT_INERTIA_FRONT 0.05 kg⋅m² Front wheel rotational inertia Physics education materials
DEFAULT_INERTIA_REAR 0.07 kg⋅m² Rear wheel rotational inertia Physics education materials
DEFAULT_WHEEL_RADIUS 0.35 m Wheel radius (700c + 25mm tire) Standard wheel sizing (0.7 m is the diameter)
DEFAULT_DRIVETRAIN_EFFICIENCY 0.976 - Mechanical efficiency Typical modern road bike
DEFAULT_AIR_DENSITY 1.225 kg/m³ Air density at sea level ISO Standard Atmosphere (15°C, 1 atm)

Cyclist Parameters (Constants.java)

Parameter Value Unit Description Academic Source
DEFAULT_CYCLIST_MASS_KG 80 kg Total system mass (cyclist + bike) Typical recreational cyclist
DEFAULT_CYCLIST_POWER_W 280 W Sustained power output (FTP) ~3.5 W/kg intermediate level
DEFAULT_MAX_BRAKE_G 0.4 g Maximum braking deceleration Measured riders use 0.41 ± 0.07 g (ceiling 0.56-0.63 g)
DEFAULT_DRAG_COEFFICIENT 0.7 - Aerodynamic drag coefficient “Aerodynamic drag in cycling” studies
DEFAULT_FRONTAL_AREA 0.5 m² Cyclist frontal area Professional cycling aerodynamics
DEFAULT_MAX_LEAN_ANGLE_DEG 35 ° Maximum cornering lean angle (µ = tan θ = 0.70) Brandt ; Zignoli 2020 (µ 0.90 dry / 0.36 wet)
DEFAULT_MAX_SPEED_KMH 100 km/h Maximum speed capability Reasonable road cycling limit

Parameter Validation:

PowerComputer: Core Physics Engine

The PowerComputer class implements the fundamental physics equations for virtual cyclist power calculations, handling acceleration, deceleration, and energy conservation.

Core Equations

1. Power Balance Equation

Formula: P_total = P_cyclist - P_resistances

Implementation: getNewPower() method (line 21)

double pSum = 0;
for (PowerProvider provider : providers.getPowerProviders()) {
    double w = provider.getPowerW(course, current);
    pSum = pSum + w;  // Sums all resistance powers (negative values)
}

Resistances included:

2. Kinetic Energy Power Equation

Formula: P = 0.5 * M_eq * (v₂² - v₁²) / Δt

Implementation: getTotPower() method (line 75)

return 0.5 * equivalentMass * (s2 * s2 - s1 * s1) / dt;

Physical meaning:

3. Equivalent Mass Calculation

Formula: M_eq = m + I_total / r²

Implementation: getEquivalentMass() method (line 83)

double mKg = course.getCyclist().getMKg();                    // Total mass (cyclist + bike)
double inertiaFront = course.getBike().getInertiaFront();    // Front wheel inertia
double inertiaRear = course.getBike().getInertiaRear();      // Rear wheel inertia  
double wheelRadius = course.getBike().getWheelRadius();      // Wheel radius
double inertia = inertiaFront + inertiaRear;                 // Combined wheel inertia
return mKg + (inertia / (wheelRadius * wheelRadius));       // Equivalent mass

Physical meaning:

4. Speed Integration from Power

Formula: v_new = √(2ΔtP/(M_eq) + v_old²)

Implementation: getDx() method (line 32)

// From: P = 0.5 * M_eq * (v_new² - v_old²) / Δt
// Solving for v_new: v_new² = 2ΔtP/M_eq + v_old²
double newSpeed = Math.max(
    Math.sqrt(dt * pSum / (0.5 * equivalentMass) + currentSpeed * currentSpeed), 
    Constants.MINIMAL_SPEED);  // 2 km/h minimum
return (newSpeed + currentSpeed) * dt / 2;  // Distance via trapezoidal integration

5. Time Integration (Inverse Problem)

Implementation: getDt() method using binary search (line 40)

// Given: distance dx, power P, current speed
// Find: time dt such that getDx(P, M_eq, v_current, dt) = dx
// Uses binary search with tolerance dx/10,000,000

6. Cyclist Power Calculation

Implementation: computeCyclistPower() method (line 58)

double power = getNewPower(course, p1, false);           // Sum all resistances  
double tot_power = getTotPower(equivalentMass, s1, s2, dt); // Power from acceleration
double cyclistPower = tot_power - power;                 // Net cyclist power
cyclistPower = Math.max(0.0, cyclistPower);             // No negative power
cyclistPower = cyclistPower / course.getBike().getEfficiency(); // Account for drivetrain losses

Academic Validation

Sources:

Key validation points: ✅ Equivalent mass formula matches rotational dynamics theory
✅ Power-velocity relationship validated by Martin et al. (3% accuracy)
✅ Energy conservation maintained throughout calculations
✅ Numerical methods ensure physical constraints (minimum speed, positive power)

Constants:

Cyclist

Virtual cyclist

Simulates a virtual cyclist on a give path.

Steps :

Fix elevation

Uses Mapterhorn terrain-RGB tiles (Terrarium-encoded WebP) to compute elevation by bilinear interpolation across 4 surrounding pixels at zoom 12 (~10 m horizontal resolution).

Tiles are fetched from https://tiles.mapterhorn.com/{z}/{x}/{y}.webp and cached on disk under <cache>/mapterhorn/{z}/{x}/{y}.webp. The base URL, zoom level, tile size, in-memory LRU capacity, and User-Agent are all configurable via MapterhornConfig.

Attribution

Elevation data shown by gpx2web is © Mapterhorn and its upstream sources. Mapterhorn is built from many national elevation datasets, each with its own license (mostly CC BY 4.0). When you display, redistribute, or publish maps or analyses produced with this tool, you must credit Mapterhorn and the underlying data providers as listed in the canonical attribution document.

If you self-host Mapterhorn tiles or point gpx2web at a private mirror via MapterhornConfig.tileUrlTemplate, you remain responsible for honouring the upstream licenses of every source dataset you use.

MaxSpeedComputer: Cornering and Braking Limits

The MaxSpeedComputer calculates safe maximum speeds considering cornering physics and braking constraints through a two-pass algorithm.

Two-Pass Algorithm

Pass 1 (Forward): Cornering Speed Limits

Implementation: firstPass() and computeMaxSpeedByIncline() methods

Algorithm:

  1. Three-point curvature analysis (lines 65-74):
    Vector circleCenter = getCircleCenter(pointBefore, currentPoint, pointAfter);
    double radius = Math.hypot(circleCenter.x(), circleCenter.y());
    radius = radius + 2; // Add 2m safety margin for trajectory
    
  2. Bicycle leaning dynamics (lines 79-81):
    // https://en.wikipedia.org/wiki/Bicycle_and_motorcycle_dynamics#Leaning
    double vmax = Math.sqrt(Constants.G * radius * cyclist.getTanMaxAngle());
    p.setSpeedMax(Math.min(cyclist.getMaxSpeedMs(), vmax));
    

Physics:

Pass 2 (Backward): Braking Constraints

Implementation: secondPass() and computeMaxSpeedByBraking() methods

Algorithm:

  1. Braking distance calculation (lines 89-96):
    double t = (vf - v0) / a;           // Time to brake from v0 to vf
    double dBrake = v0 * t + (a * t * t) / 2;  // Distance needed to brake
    
  2. Speed adjustment (lines 97-103):
    if (dBrake > dist) {
        // Not enough distance to brake safely
        double newMaxSpeedPrevious = Math.sqrt(vf * vf - 2 * a * dist);
        pm1.setSpeedMax(newMaxSpeedPrevious);
    }
    

Physics:

Mathematical Components

Circle Center Calculation

Implementation: getCircleCenter() method (lines 106-128)

// Solves system of equations for circle through three points
double G = 2 * (A * (cy - by) - B * (cx - bx));
if (Math.abs(G) < 0.001) return null; // Collinear points
double px = (D * E - B * F) / G;
double py = (A * F - C * E) / G;

Handles edge cases:

Coordinate Transformation

Implementation: transform() method (lines 130-136)

double x = (lon / (2 * Math.PI)) * Constants.CIRC * Math.cos(pRef.getLat());
double y = (lat / (2 * Math.PI)) * Constants.CIRC;

Purpose: Converts GPS coordinates (lat/lon) to local Cartesian coordinates (meters) for geometric calculations.

Academic Validation

Sources:

Key physics equations:

Safety margins:

VirtualizeService: Complete Course Simulation

The VirtualizeService orchestrates the complete virtual cyclist simulation, integrating power calculations, speed constraints, and temporal progression to generate a realistic ride profile.

Core Algorithm: Time-Stepping Integration

Implementation: virtualizeTrack() method combining all physics components

Initialization Phase (lines 33-53)

OptimalSpeeds optimalSpeeds = new OptimalSpeeds(optimalSpeedService, course);
double equivalentMass = powerComputer.getEquivalentMass(course);

Point current = input.get(0).copy();
current.setSpeed(Constants.MINIMAL_SPEED);  // Start at 2 km/h

Setup:

Main Integration Loop (lines 55-93)

Time-stepping algorithm with adaptive step size:

  1. Power calculation (line 60):
    double pSum = powerComputer.getNewPower(course, current, true);
    
    • Sums all resistance forces: rolling, bearings, gravity, aerodynamic
    • Includes cyclist power in balance
  2. Distance prediction (line 61):
    double dx = powerComputer.getDx(pSum, equivalentMass, currentSpeed, DT);
    
    • Uses kinetic energy equation: dx = (v_new + v_old) * dt / 2
    • Where v_new = √(2ΔtP/M_eq + v_old²)
  3. Adaptive step handling (lines 66-79):
    if (index != newIndex) {
        // Step crosses GPS waypoint - adjust to exact waypoint
        dxToNext = input.get(newIndex).getDist() - current.getDist();
        dtToNext = powerComputer.getDt(pSum, equivalentMass, currentSpeed, dxToNext);
        current = input.get(newIndex).copy();
    } else {
        // Regular time step
        dxToNext = dx;
        dtToNext = DT;
        current = Point.interpolate(input.get(index), input.get(index + 1), coef);
    }
    
  4. Speed integration with constraints (lines 81-85):
    double speedNew = 2 * (dxToNext / dtToNext) - currentSpeed;  // Trapezoidal rule
    if (speedNew > current.getSpeedMax()) {
        speedNew = current.getSpeedMax();  // Apply speed limits
    }
    dtToNext = 2 * dxToNext / (currentSpeed + speedNew);  // Adjust time
    
  5. Temporal progression (lines 89-92):
    Duration dtToNextDuration = getDuration(dtToNext);
    now = now.plus(dtToNextDuration);
    current.setInstant(start, now);
    

Post-Processing Phase (lines 95-99)

for (int i = 0; i < newPoints.size() - 1; i++) {
    double cyclistPower = powerComputer.computeCyclistPower(course, equivalentMass, 
                                                           newPoints.get(i), newPoints.get(i + 1));
    newPoints.get(i).setPower(cyclistPower);
}

Cyclist power calculation:

Numerical Methods Validation

Primary integration method: Modified trapezoidal rule

Academic validation:

Advanced Features

Binary Search Distance Lookup (lines 117-135)

private int getIndex(double[] dists, int distsLength, double dist) {
    // O(log n) search for GPS point index at given distance
    while (left <= right) {
        int mid = left + (right - left) / 2;
        // Binary search implementation...
    }
}

GPS Point Interpolation

Boundary Conditions

Academic Foundation

Numerical integration theory:

Cycling simulation research:

Output: Complete time-series cycling simulation with:

Heart Rate Simulation: Exercise Physiology Modeling

The HRSimulator generates realistic heart rate data using machine learning trained on real cycling data, incorporating temporal dynamics and physiological constraints.

Core Architecture

Implementation: Linear regression model using SMILE machine learning library

Training Phase (lines 43-56)

public void train(final List<GPXPath> samples) {
    List<DataPoint> dataPoints = new ArrayList<>();
    for (GPXPath sample : samples) {
        getDataPoints(dataPoints, sample);  // Extract features every 5 seconds
    }
    this.linearModel = OLS.fit(Formula.lhs("hr"), DataFrame.of(dataPoints));
    // Model serialized to resources/hrmodel
}

Training features:

Feature Engineering: Temporal Heart Rate and Power

DataPoint features (DataPoint.java:11-22):

// Heart rate features - temporal lookback
new Field("hr", PropertyKeys.heartRate, 0.0),      // Current HR
new Field("hr5", PropertyKeys.heartRate, 5.0),     // HR 5 seconds ago  
new Field("hr10", PropertyKeys.heartRate, 10.0),   // HR 10 seconds ago
new Field("hr30", PropertyKeys.heartRate, 30.0),   // HR 30 seconds ago

// Power features - moving averages  
new Field("p5", PropertyKeys.power, 0.0, 5.0),     // 5-second power average
new Field("p10", PropertyKeys.power, 0.0, 10.0),   // 10-second power average
new Field("p60", PropertyKeys.power, 0.0, 60.0),   // 60-second power average

Feature extraction (Field.java:23-25):

double getValue(GPXPath gpxPath, double t) {
    double average = gpxPath.getAverage(t - start, t - end, propertyKey);
    return hr ? clamp(average) : average;  // HR clamped to 60-220 bpm
}

Simulation Phase (lines 58-70)

public void simulateHeartRate(final GPXPath gpxPath) {
    for (int i = 0; i < points.size() - 1; i++) {
        double t = points.get(i).getElapsedSeconds();
        DataPoint dataPoint = getDataPoint(gpxPath, t);      // Extract features
        double hr = linearModel.predict(dataPoint);         // ML prediction
        points.get(i).setHeartRate(hr);
    }
    smoothService.smoothHr(gpxPath);  // Post-processing smoothing
}

Exercise Physiology Validation

Academic foundation:

Key research findings:

  1. Power-HR linearity: Linear at low-submaximal intensities, validated by multiple studies
  2. Temporal dynamics: HR remains elevated ~15 minutes post-effort, captured by lookback features
  3. Individual variation: Linear models capture person-specific fitness adaptations
  4. Smoothing techniques: Post-processing smoothing standard in cycling performance analysis

Model Architecture Analysis

Strengths of Current Approach:

Limitations and Future Improvements:

Random Forest Improvement Potential

Why Random Forest would improve results:

  1. Non-linear relationships: Captures curvilinear HR response at high intensities
  2. Feature interactions: Models complex interactions between power, time, and environmental factors
  3. Individual patterns: Better handles person-specific physiological responses
  4. Robustness: Less sensitive to outliers and data quality issues
  5. Feature importance: Provides insight into which factors most influence HR

Expected improvements:

Academic References

Exercise physiology sources:

Current implementation: Simple but physiologically grounded approach suitable for basic simulation. Random forest upgrade would provide significantly more realistic and personalized heart rate modeling.

GPXEnhancer: Complete Virtualization Pipeline

The GPXEnhancer orchestrates the complete virtual cyclist simulation pipeline, transforming raw GPS tracks into realistic cycling experiences with proper data preprocessing and post-processing.

Pipeline Architecture

Main method: virtualize(Course course, boolean filter) (lines 67-78)

Step-by-Step Pipeline Analysis:

1. Distance-based Resampling (line 69)

gpxPerDistance.computeOnePointPerDistance(gpxPath, 10.0);

2. Elevation Correction (line 70)

gpxElevationFixer.fixElevation(gpxPath);

3. Speed Constraint Calculation (line 71)

maxSpeedComputer.computeMaxSpeeds(course);

4. Physics-based Simulation (line 72)

virtualizeService.virtualizeTrack(course);

5. Heart Rate Modeling (line 73)

hrSimulator.simulateHeartRate(gpxPath);

6. Temporal Resampling (line 74)

gpxPerSecond.computeOnePointPerSecond(gpxPath);

7. Optional Track Simplification (lines 75-77)

if (filter) {
    GPXFilter.filterPointsDouglasPeucker(gpxPath);
}

Pipeline Validation

Academic foundations:

Step sequencing rationale:

  1. Distance resampling first: Establishes consistent spatial intervals before physics
  2. Elevation fixing early: Required for accurate grade calculation throughout pipeline
  3. Max speeds before simulation: Provides constraints for physics engine
  4. Heart rate after simulation: Depends on power output from virtualization
  5. Temporal resampling last: Formats physics output for standard compatibility
  6. Filtering optional: Final step to optimize output size without affecting physics

Course Construction (lines 49-64)

Default configuration setup:

Cyclist cyclist = Cyclist.getDefault();        // Standard cyclist parameters
Bike bike = Bike.getDefault();                 // Standard bike parameters
Course course = new Course(
    gpxPath, instant, cyclist, bike,
    new PowerProviderConstant(),               // Constant power capability
    new WindProviderNone(),                    // No wind conditions  
    new AeroProviderConstant()                 // Constant aerodynamics
);

Multi-track support (lines 39-43):

Performance and Scalability

Computational complexity:

Memory efficiency:

Quality Assurance

Data validation at each step:

Error handling:

The GPXEnhancer pipeline represents a comprehensive approach to virtual cyclist simulation, combining established GPS processing techniques with validated cycling physics models to produce realistic and accurate virtual cycling experiences.