From ec958ce6c875ce9ff1bc447727a364710de7d078 Mon Sep 17 00:00:00 2001
From: Freezed <2160318-free_zed@users.noreply.gitlab.com>
Date: Sun, 27 Mar 2022 18:47:38 +0200
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Get=20GPX=20data=20summary=20from?=
 =?UTF-8?q?=20file(s)=20#2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Relies on `gpxpy` library
- Can be used as a standalone module

TODO: remove print() by a logger (see #11)
---
 README.md        |   3 +-
 cli/gpx.py       | 124 +++++++++++++++++++++++++++++++++++++++++++++++
 requirements.txt |   1 +
 3 files changed, 127 insertions(+), 1 deletion(-)
 create mode 100755 cli/gpx.py

diff --git a/README.md b/README.md
index e09aaaf..dc196be 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,8 @@ python cli/run.py --help
 
 Details in [`requirements.txt`](requirements.txt) & [`requirements-dev.txt`](requirements-dev.txt)
 
-- [`strava-cli`](https://github.com/bwilczynski/strava-cli)
+- [`gpxpy`](https://github.com/tkrajina/gpxpy/#readme) : GPX file manipulation
+- [`strava-cli`](https://github.com/bwilczynski/strava-cli#readme) : Use Strava's API on command line
 
 
 ### 🤝 Contributing
diff --git a/cli/gpx.py b/cli/gpx.py
new file mode 100755
index 0000000..7ba7e6c
--- /dev/null
+++ b/cli/gpx.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# coding:utf-8
+"""GPX manipulation module.
+
+Process a GPX file to get informations.
+"""
+
+import sys as mod_sys
+import logging as mod_logging
+from typing import Union, List
+
+import gpxpy as mod_gpxpy
+import gpxpy.gpx as mod_gpx
+
+
+def get_gpx_part_info(
+    gpx_part: Union[mod_gpx.GPX, mod_gpx.GPXTrack, mod_gpx.GPXTrackSegment],
+) -> None:
+    """gpx_part may be a track or segment."""
+    gpx_part_summary = {}
+
+    gpx_part_summary.update({"length_2d": gpx_part.length_2d() or 0})
+    gpx_part_summary.update({"": gpx_part.length_3d()})
+
+    moving_data = gpx_part.get_moving_data()
+    if moving_data:
+        gpx_part_summary.update({"moving_time": moving_data.moving_time})
+        gpx_part_summary.update({"stopped_time": moving_data.stopped_time})
+        gpx_part_summary.update({"max_speed": moving_data.max_speed})
+        gpx_part_summary.update(
+            {
+                "avg_speed": moving_data.moving_distance / moving_data.moving_time
+                if moving_data.moving_time > 0
+                else "?"
+            }
+        )
+
+    uphill, downhill = gpx_part.get_uphill_downhill()
+    gpx_part_summary.update({"uphill": uphill})
+    gpx_part_summary.update({"downhill": downhill})
+
+    start_time, end_time = gpx_part.get_time_bounds()
+    gpx_part_summary.update({"start_time": start_time})
+    gpx_part_summary.update({"end_time": end_time})
+
+    points_no = len(list(gpx_part.walk(only_points=True)))
+    gpx_part_summary.update({"point_no": points_no})
+
+    if points_no > 0:
+        distances: List[float] = []
+        previous_point = None
+        for point in gpx_part.walk(only_points=True):
+            if previous_point:
+                distance = point.distance_2d(previous_point)
+                distances.append(distance)
+            previous_point = point
+        gpx_part_summary.update(
+            {"avg_dist_points": sum(distances) / len(list(gpx_part.walk()))}
+        )
+
+    return gpx_part_summary
+
+
+def get_gpx_info(gpx: mod_gpx.GPX, gpx_file: str) -> None:
+    """Get all information of the file.
+
+    Top level info & whole track move info are agregated.
+    If the file had more than one track/segment, move info are
+    calculated over all.
+    """
+    gpx_summary = {}
+
+    if gpx.name:
+        gpx_summary.update({"name": gpx.name})
+    if gpx.description:
+        gpx_summary.update({"description": gpx.description})
+    if gpx.author_name:
+        gpx_summary.update({"author_name": gpx.author_name})
+    if gpx.author_email:
+        gpx_summary.update({"author_email": gpx.author_email})
+
+    gpx_summary.update({"file_name": gpx_file})
+    gpx_summary.update({"waypoints": len(gpx.waypoints)})
+    gpx_summary.update({"routes": len(gpx.routes)})
+    gpx_summary.update({"tracks": len(gpx.tracks)})
+    # gpx_summary.update({"segments": len(track.segments)})
+
+    gpx_summary.update(get_gpx_part_info(gpx))
+    return gpx_summary
+
+
+def run(gpx_files: List[str]) -> None:
+    """Manage GPX file in input."""
+    if not gpx_files:
+        mod_logging.exception("No GPX files given")
+        mod_sys.exit(1)
+
+    for gpx_file in gpx_files:
+        with open(gpx_file, encoding="utf-8") as open_file:
+            gpx = mod_gpxpy.parse(open_file)
+
+            try:
+                return get_gpx_info(gpx, gpx_file)
+            except mod_gpx.GPXXMLSyntaxException as except_detail:
+                mod_logging.exception(except_detail)
+                mod_sys.exit(1)
+            except FileNotFoundError as except_detail:
+                mod_logging.exception(except_detail)
+                mod_sys.exit(1)
+            except Exception as except_detail:  # pylint: disable=W0703
+                mod_logging.exception(except_detail)
+                mod_sys.exit(1)
+
+
+if __name__ == "__main__":
+    import argparse as mod_argparse
+
+    parser = mod_argparse.ArgumentParser(
+        usage="%(prog)s [-h] path_to_gpx_file(s)",
+        description="Command line utility to extract basic statistics from gpx file(s)",
+    )
+    args, local_files = parser.parse_known_args()
+
+    print(run(gpx_files=local_files))
diff --git a/requirements.txt b/requirements.txt
index 19d8a2a..ac074f1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 click==8.0.4
+gpxpy==1.5.0
 strava-cli==0.6.1
-- 
GitLab