Created
March 16, 2026 15:26
-
-
Save JamesGardiner/bcf6d6b626d911f1f0a86d19fc8f097c to your computer and use it in GitHub Desktop.
Extract stats from a GPX file for Mountain Training QMD logging (uv script)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # requires-python = ">=3.12" | |
| # dependencies = ["gpxpy"] | |
| # /// | |
| """Extract stats from a GPX file for Mountain Training QMD logging.""" | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| import gpxpy | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Extract stats from a GPX file for QMD logging" | |
| ) | |
| parser.add_argument("gpx_file", type=Path, help="Path to GPX file") | |
| args = parser.parse_args() | |
| if not args.gpx_file.exists(): | |
| print(f"Error: {args.gpx_file} not found", file=sys.stderr) | |
| sys.exit(1) | |
| with open(args.gpx_file) as f: | |
| gpx = gpxpy.parse(f) | |
| bounds = gpx.get_bounds() | |
| time_bounds = gpx.get_time_bounds() | |
| moving = gpx.get_moving_data() | |
| uphill, downhill = gpx.get_uphill_downhill() | |
| ele_extremes = gpx.get_elevation_extremes() | |
| # Activity name | |
| name = gpx.tracks[0].name if gpx.tracks and gpx.tracks[0].name else "Unknown" | |
| # Duration | |
| duration = gpx.get_duration() | |
| if duration: | |
| hours = int(duration // 3600) | |
| mins = int((duration % 3600) // 60) | |
| duration_str = f"{hours}h {mins}m" | |
| else: | |
| duration_str = "Unknown" | |
| # Moving time | |
| if moving and moving.moving_time: | |
| mh = int(moving.moving_time // 3600) | |
| mm = int((moving.moving_time % 3600) // 60) | |
| moving_str = f"{mh}h {mm}m" | |
| else: | |
| moving_str = "Unknown" | |
| # Distance | |
| length_2d = gpx.length_2d() / 1000 | |
| length_3d = gpx.length_3d() / 1000 | |
| # Start point | |
| start_lat = bounds.min_latitude if bounds else None | |
| start_lon = bounds.min_longitude if bounds else None | |
| first_point = None | |
| for track in gpx.tracks: | |
| for seg in track.segments: | |
| if seg.points: | |
| first_point = seg.points[0] | |
| break | |
| if first_point: | |
| break | |
| print(f"Activity: {name}") | |
| if time_bounds.start_time: | |
| print(f"Date: {time_bounds.start_time.strftime('%Y-%m-%d')}") | |
| print(f"Start time: {time_bounds.start_time.strftime('%H:%M:%S %Z')}") | |
| if time_bounds.end_time: | |
| print(f"End time: {time_bounds.end_time.strftime('%H:%M:%S %Z')}") | |
| print(f"Duration: {duration_str}") | |
| print(f"Moving time: {moving_str}") | |
| print(f"Distance (2D): {length_2d:.1f} km") | |
| print(f"Distance (3D): {length_3d:.1f} km") | |
| if ele_extremes: | |
| print(f"Max elevation: {ele_extremes.maximum:.0f} m") | |
| print(f"Min elevation: {ele_extremes.minimum:.0f} m") | |
| print(f"Elevation gain: {uphill:.0f} m (GPX - noisy, use Garmin corrected value)") | |
| print(f"Elevation loss: {downhill:.0f} m (GPX - noisy, use Garmin corrected value)") | |
| if first_point: | |
| print(f"Start coords: {first_point.latitude:.5f}, {first_point.longitude:.5f}") | |
| print() | |
| # QMD criteria checks | |
| print("--- QMD Criteria Pre-fill ---") | |
| if ele_extremes and ele_extremes.maximum >= 600: | |
| print(f"Mountain ascended: YES (max elevation {ele_extremes.maximum:.0f} m)") | |
| else: | |
| max_e = ele_extremes.maximum if ele_extremes else 0 | |
| print(f"Mountain ascended: NO (max elevation {max_e:.0f} m, need >= 600 m)") | |
| if duration and duration >= 5 * 3600: | |
| print(f"Five hours+: YES ({duration_str})") | |
| elif duration: | |
| print(f"Five hours+: NO ({duration_str}, need >= 5h)") | |
| else: | |
| print("Five hours+: UNKNOWN") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment