Skip to content

Commit

Permalink
GPX Manipulation and tests refactoring, fix examples
Browse files Browse the repository at this point in the history
  • Loading branch information
MihaMi27 committed Dec 15, 2024
1 parent b5b8e5a commit 2f5bc2e
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 35 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,11 @@ from sport_activities_features.gpx_manipulation import GPXFile
gpx_file=GPXFile()

# Read the file and generate a dictionary with
data = gpx_file.read_one_file("path_to_the_file") # Represents data as dictionary of lists
gpx_exercise = gpx_file.read_one_file("path_to_the_file")
data = gpx_file.extract_activity_data(gpx_exercise) # Represents data as dictionary of lists

# Alternative choice
data = gpx_file.read_one_file("path_to_the_file", numpy_array= True) # Represents data as dictionary of numpy.arrays
data = gpx_file.extract_activity_data(gpx_exercise, numpy_array= True) # Represents data as dictionary of numpy.arrays

```

Expand Down
5 changes: 3 additions & 2 deletions examples/convert_gpx_to_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
output_file = 'path_to_output_file'

# Read GPX file
data = gpx_file.read_one_file(
input_file,
gpx_exercise = gpx_file.read_one_file(input_file)
data = gpx_file.extract_activity_data(
gpx_exercise,
) # Represents data as dictionary of lists

# Convert dictionary of lists to pandas DataFrame
Expand Down
3 changes: 2 additions & 1 deletion examples/read_gpx_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

# read GPX file
gpx_file = GPXFile()
data = gpx_file.read_one_file('path_to_the_file')
gpx_exercise = gpx_file.read_one_file('path_to_the_file')
data = gpx_file.extract_activity_data(gpx_exercise)
191 changes: 163 additions & 28 deletions sport_activities_features/gpx_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import geopy
import gpxpy
import gpxpy.gpx
import numpy as np

from sport_activities_features.file_manipulation import FileManipulation
Expand Down Expand Up @@ -107,14 +108,115 @@ def from_GPX(
self.cadence = cadence
self.watts = watts

class GPXExercise:
"""Class for storing exercise data from a GPX file.\n
Args:
raw_data (GPX):
raw data from the GPX file.
trackpoints (list):
list of GPXTrackPoint objects.
activity_type (str):
type of the activity (e.g. Biking).
distance (float):
total distance of exercise in meters.
duration (float):
duration of exercise in seconds.
calories (int):
calories burned during the exercise.
hr_avg (float):
average heart rate during the exercise.
hr_max (int):
maximum heart rate during the exercise.
hr_min (int):
minimum heart rate during the exercise.
altitude_avg (float):
average altitude in meters.
altitude_max (float):
maximum altitude in meters.
altitude_min (float):
minimum altitude in meters.
ascent (float):
total ascent in meters.
descent (float):
total descent in meters.
"""

class GPXFile(FileManipulation):
"""Class for reading GPX files."""
def __init__(self, raw_data: gpxpy.gpx.GPX, trackpoints: list = None) -> None:
"""Initialisation method for GPXExercise class.\n
Args:
raw_data (GPX):
raw data from the GPX file.
trackpoints (list):
list of GPXTrackPoint objects.
"""
if trackpoints is None:
trackpoints = []
self.trackpoints = trackpoints
self.raw_data = raw_data
self._calculate_values()

def _calculate_values(self):
gpx = self.raw_data
try:
self.activity_type = gpx.tracks[0].type
except BaseException:
self.activity_type = None

def __init__(self) -> None:
"""Initialisation method for GPXFile class."""
self.all_files = []
try:
self.distance = gpx['total_distance']
except BaseException:
self.distance = None

try:
self.duration = gpx['timestamps'][-1] - gpx['timestamps'][0]
except BaseException:
self.duration = None

try:
self.calories = None
except BaseException:
self.calories = None

try:
self.hr_avg = sum(gpx['heartrates']) / len(gpx['heartrates'])
except BaseException:
self.hr_avg = None

try:
self.hr_max = max(gpx['heartrates'])
except BaseException:
self.hr_max = None

try:
self.hr_min = min(gpx['heartrates'])
except BaseException:
self.hr_min = None

try:
self.altitude_avg = sum(gpx['altitudes']) / len(gpx['altitudes'])
except BaseException:
self.altitude_avg = None

try:
self.altitude_max = max(gpx['altitudes'])
except BaseException:
self.altitude_max = None

try:
self.altitude_min = min(gpx['altitudes'])
except BaseException:
self.altitude_min = None

try:
self.ascent = self.__ascent(gpx['altitudes'])
except BaseException:
self.ascent = None

try:
self.descent = self.__descent(gpx['altitudes'])
except BaseException:
self.descent = None

def __ascent(self, altitudes: list) -> int:
"""Method for calculating the total ascent from a list of altitudes.\n
Args:
Expand Down Expand Up @@ -143,6 +245,15 @@ def __descent(self, altitudes: list) -> int:
descent += altitudes[index - 1] - altitude
return descent


class GPXFile(FileManipulation):
"""Class for reading GPX files."""

def __init__(self) -> None:
"""Initialisation method for GPXFile class."""
self.all_files = []


def read_directory(self, directory_name: str) -> list:
"""Method for finding all GPX files in a directory.\n
Args:
Expand All @@ -158,13 +269,11 @@ def read_directory(self, directory_name: str) -> list:
self.all_files.append(file)
return self.all_files

def read_one_file(self, filename, numpy_array=False):
def read_one_file(self, filename: str) -> GPXExercise:
"""Method for parsing one GPX file.\n
Args:
filename (str):
name of the TCX file to be read
numpy_array (bool):
if set to true dictionary lists are transformed into numpy arrays
Returns:
activity (dict):
{
Expand All @@ -183,7 +292,7 @@ def read_one_file(self, filename, numpy_array=False):
"""
NAMESPACE = '{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}'
points = []
gpx = None
gpx = None
try:
gpx_file = open(filename, encoding='utf-8')
gpx = gpxpy.parse(gpx_file)
Expand Down Expand Up @@ -238,9 +347,36 @@ def read_one_file(self, filename, numpy_array=False):
previous_point = trackpoint

gpx_file.close()
gpx_exercise = GPXExercise(gpx,points)
return gpx_exercise


def extract_activity_data(self, gpx: GPXExercise, numpy_array = False) -> dict:
"""Method for parsing one GPX file.\n
Args:
gpx (GPXExercise):
GPXExercise object to be read
numpy_array (bool):
if True, dictionary lists are transformed into numpy arrays
Returns:
activity (dict):
{
'activity_type': activity_type,
'positions': positions,
'altitudes': altitudes,
'distances': distances,
'total_distance': total_distance,
'timestamps': timestamps,
'heartrates': heartrates,
'speeds': speeds
}
Note:
In the case of missing value in raw data, we assign None.
"""
# handling missing data - should be improved in original
try:
activity_type = gpx.tracks[0].type
activity_type = gpx.raw_data.tracks[0].type
except BaseException:
activity_type = None

Expand All @@ -250,8 +386,8 @@ def read_one_file(self, filename, numpy_array=False):
timestamps = []
heartrates = []
speeds = []
trackpoint: TCXTrackPoint
for trackpoint in points:
trackpoint: GPXTrackPoint
for trackpoint in gpx.trackpoints:
positions.append((trackpoint.latitude, trackpoint.longitude))
altitudes.append(trackpoint.elevation)
distances.append(trackpoint.distance)
Expand Down Expand Up @@ -284,8 +420,8 @@ def read_one_file(self, filename, numpy_array=False):
}

return activity

def extract_integral_metrics(self, filename) -> dict:
def extract_integral_metrics(self, gpx_exercise: GPXExercise) -> dict:
"""Method for parsing one GPX file and extracting integral metrics.\n
Args:
filename (str):
Expand All @@ -306,67 +442,66 @@ def extract_integral_metrics(self, filename) -> dict:
"ascent": ascent,
"descent": descent,
}.
"""
gpx = self.read_one_file(filename)
"""

# handling missing data in raw files
try:
activity_type = gpx.tracks[0].type
activity_type = gpx_exercise.activity_type
except BaseException:
activity_type = None

try:
distance = gpx['total_distance']
distance = gpx_exercise.distance
except BaseException:
distance = None

try:
duration = gpx['timestamps'][-1] - gpx['timestamps'][0]
duration = gpx_exercise.duration
except BaseException:
duration = None

try:
calories = None
calories = gpx_exercise.calories
except BaseException:
calories = None

try:
hr_avg = sum(gpx['heartrates']) / len(gpx['heartrates'])
hr_avg = gpx_exercise.hr_avg
except BaseException:
hr_avg = None

try:
hr_max = max(gpx['heartrates'])
hr_max = gpx_exercise.hr_max
except BaseException:
hr_max = None

try:
hr_min = min(gpx['heartrates'])
hr_min = gpx_exercise.hr_min
except BaseException:
hr_min = None

try:
altitude_avg = sum(gpx['altitudes']) / len(gpx['altitudes'])
altitude_avg = gpx_exercise.altitude_avg
except BaseException:
altitude_avg = None

try:
altitude_max = max(gpx['altitudes'])
altitude_max = gpx_exercise.altitude_max
except BaseException:
altitude_max = None

try:
altitude_min = min(gpx['altitudes'])
altitude_min = gpx_exercise.altitude_min
except BaseException:
altitude_min = None

try:
ascent = self.__ascent(gpx['altitudes'])
ascent = gpx_exercise.ascent
except BaseException:
ascent = None

try:
descent = self.__descent(gpx['altitudes'])
descent = gpx_exercise.descent
except BaseException:
descent = None

Expand Down
6 changes: 4 additions & 2 deletions tests/test_gpx_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def setUp(self):
os.path.dirname(__file__), 'data', 'riderx3.gpx',
)
self.gpx_file = GPXFile()
self.data = self.gpx_file.read_one_file(filename)
gpx_exercise = self.gpx_file.read_one_file(filename)
self.data = self.gpx_file.extract_activity_data(gpx_exercise)

def test_total_distance(self):
self.assertAlmostEqual(self.data['total_distance'], 5774.703, 2)
Expand Down Expand Up @@ -39,7 +40,8 @@ def test_utf8_formatting(self):
]
for index, f in enumerate(utf8_filenames):
filename = os.path.join(os.path.dirname(__file__), 'data', f)
data = self.gpx_file.read_one_file(filename)
gpx_exercise = self.gpx_file.read_one_file(filename)
data = self.gpx_file.extract_activity_data(gpx_exercise)
self.assertAlmostEqual(
data['total_distance'], utf8_distances[index], places=5,
)

0 comments on commit 2f5bc2e

Please sign in to comment.