├── .all-contributorsrc ├── .gitattributes ├── .github ├── images │ └── data-explanation.svg ├── logo │ ├── logo-no-background.png │ └── logo-white-background.png └── workflows │ ├── main.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── example_data ├── 15.tcx ├── __init__.py └── sup_activity_1.tcx ├── examples ├── __init__.py └── reader_demo.py ├── index.html ├── pyproject.toml ├── setup.py ├── tcxreader ├── __init__.py ├── tcx_author.py ├── tcx_exercise.py ├── tcx_lap.py ├── tcx_track_point.py ├── tcxreader.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── data │ ├── 15.tcx │ ├── cross-country-skiing_activity_1.tcx │ ├── mapmyride_biking.tcx │ └── sup_activity_1.tcx │ └── test_reader.py └── tests ├── __init__.py └── test_tcxreader.py /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitConvention": "angular", 8 | "contributors": [ 9 | { 10 | "login": "alenrajsp", 11 | "name": "alenrajsp", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/27721714?v=4", 13 | "profile": "https://github.com/alenrajsp", 14 | "contributions": [ 15 | "code", 16 | "maintenance" 17 | ] 18 | }, 19 | { 20 | "login": "fortysix2ahead", 21 | "name": "fortysix2ahead", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/40423757?v=4", 23 | "profile": "https://github.com/fortysix2ahead", 24 | "contributions": [ 25 | "bug" 26 | ] 27 | }, 28 | { 29 | "login": "firefly-cpp", 30 | "name": "Iztok Fister Jr.", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/1633361?v=4", 32 | "profile": "http://www.iztok-jr-fister.eu/", 33 | "contributions": [ 34 | "data", 35 | "mentoring", 36 | "platform", 37 | "test" 38 | ] 39 | }, 40 | { 41 | "login": "johnleeming", 42 | "name": "johnleeming", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/13801070?v=4", 44 | "profile": "https://github.com/johnleeming", 45 | "contributions": [ 46 | "bug" 47 | ] 48 | }, 49 | { 50 | "login": "rpstar", 51 | "name": "rpstar", 52 | "avatar_url": "https://avatars.githubusercontent.com/u/10442282?v=4", 53 | "profile": "https://github.com/rpstar", 54 | "contributions": [ 55 | "bug" 56 | ] 57 | }, 58 | { 59 | "login": "jlrobins", 60 | "name": "James Robinson", 61 | "avatar_url": "https://avatars.githubusercontent.com/u/1128313?v=4", 62 | "profile": "http://relatable.dev/", 63 | "contributions": [ 64 | "maintenance" 65 | ] 66 | }, 67 | { 68 | "login": "johwiebe", 69 | "name": "johwiebe", 70 | "avatar_url": "https://avatars.githubusercontent.com/u/33023818?v=4", 71 | "profile": "https://github.com/johwiebe", 72 | "contributions": [ 73 | "bug" 74 | ] 75 | }, 76 | { 77 | "login": "martin-ueding", 78 | "name": "Martin Ueding", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/976924?v=4", 80 | "profile": "https://martin-ueding.de/", 81 | "contributions": [ 82 | "bug" 83 | ] 84 | }, 85 | { 86 | "login": "simonpickering", 87 | "name": "Simon Pickering", 88 | "avatar_url": "https://avatars.githubusercontent.com/u/1830341?v=4", 89 | "profile": "https://github.com/simonpickering", 90 | "contributions": [ 91 | "bug" 92 | ] 93 | }, 94 | { 95 | "login": "rwinklerwilkes", 96 | "name": "Rich Winkler", 97 | "avatar_url": "https://avatars.githubusercontent.com/u/2768609?v=4", 98 | "profile": "https://github.com/rwinklerwilkes", 99 | "contributions": [ 100 | "bug" 101 | ] 102 | }, 103 | { 104 | "login": "ToonElewaut", 105 | "name": "Toon Elewaut", 106 | "avatar_url": "https://avatars.githubusercontent.com/u/12350289?v=4", 107 | "profile": "https://github.com/ToonElewaut", 108 | "contributions": [ 109 | "code" 110 | ] 111 | }, 112 | { 113 | "login": "lahovniktadej", 114 | "name": "Tadej Lahovnik", 115 | "avatar_url": "https://avatars.githubusercontent.com/u/57890734?v=4", 116 | "profile": "https://github.com/lahovniktadej", 117 | "contributions": [ 118 | "doc" 119 | ] 120 | } 121 | ], 122 | "contributorsPerLine": 7, 123 | "skipCi": true, 124 | "repoType": "github", 125 | "repoHost": "https://github.com", 126 | "projectName": "tcxreader", 127 | "projectOwner": "alenrajsp", 128 | "commitType": "docs" 129 | } 130 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/images/data-explanation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
TCXReader
TCXReader
TCXExercise
TCXExercise
Generates from TCX file
Generates from TCX file
- activity_type: str
- altitude_avg: float
- altitude_min: float
- ascent: int
- avg_speed: float
- cadence_avg: int
- cadence_max: int
- calories: int
- descent: int
- distance: float
- duration: float
- end_time: datetime
- hr_avg: float
- hr_max: int
- hr_min: int
- start_time: datetime

- laps: TCXLap[]
- lx_ext: dict
- tpx_ext_stats: dict
- trackpoints: TCXTrackPoint[]
- activity_type: str...
TCXLap
TCXLap
- altitude_avg: float
- altitude_min: float
- ascent: int
- avg_speed: float
- cadence_avg: int
- cadence_max: int
- calories: int
- descent: int
- distance: float
- duration: float
- end_time: datetime
- hr_avg: float
- hr_max: int
- hr_min: int
- start_time: datetime

- lx_ext: dict
- tpx_ext_stats: dict
- trackpoints: TCXTrackPoint[]
- altitude_avg: float...
TCXTrackpoint
TCXTrackpoint
- cadence: int
- distance: float
- elevation: int
- hr_value: int
- latitude: float
- longitude: float
- time: datetime

- tpx_ext: dict

- cadence: int...
tpx_ext: dict
tpx_ext: dict
- key1 = value1 (int / float)
- key2 = value2 (int / float)

...
- key1 = value1 (int / float)...
tpx_ext_stats: dict
tpx_ext_stats: dict
- "key1" = { "min": int / float,
"max": int / float
"avg": int / float }
- "key2" = { "min": int / float
"max": int / float
"avg": int / float }

...
- "key1" = { "min": int / floa...
Calculated averages of all
TCXTrackPoint of an object
Calculated averages of allTC...
lx_ext: dict
lx_ext: dict
- key1 = int / float
...
- key1 = int / float...
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /.github/logo/logo-no-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alenrajsp/tcxreader/4b0daf5211a7848695918dd84c27ff44891d2e93/.github/logo/logo-no-background.png -------------------------------------------------------------------------------- /.github/logo/logo-white-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alenrajsp/tcxreader/4b0daf5211a7848695918dd84c27ff44891d2e93/.github/logo/logo-white-background.png -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Generate changelog 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Set a branch to monitor for changes 7 | 8 | jobs: 9 | generate: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Generate changelog 16 | uses: heinrichreimer/github-changelog-generator-action@v2.1.1 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | - name: Commit and push if it changed 21 | run: |- 22 | git diff 23 | git config --global user.email "changelog-bot@example.com" 24 | git config --global user.name "Changelog Bot" 25 | git diff --quiet || (git add CHANGELOG.md && git commit -m "Update CHANGELOG.md" && git push) 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tcxreader 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.9 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.9" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install flake8 pytest 24 | - name: Lint with flake8 25 | run: | 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test with pytest 31 | run: | 32 | pytest 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pycharm 2 | .idea 3 | 4 | # Vscode 5 | .vscode 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # celery beat schedule file 101 | celerybeat-schedule 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Development 116 | development 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.4.10](https://github.com/alenrajsp/tcxreader/tree/v0.4.10) (2024-04-08) 4 | 5 | ## [v0.4.9](https://github.com/alenrajsp/tcxreader/tree/v0.4.9) (2024-01-31) 6 | 7 | ## [v0.4.8](https://github.com/alenrajsp/tcxreader/tree/v0.4.8) (2024-01-31) 8 | 9 | ## [v0.4.6](https://github.com/alenrajsp/tcxreader/tree/v0.4.6) (2023-11-30) 10 | 11 | ## [v0.4.5](https://github.com/alenrajsp/tcxreader/tree/v0.4.5) (2023-11-24) 12 | 13 | ## [v0.4.4](https://github.com/alenrajsp/tcxreader/tree/v0.4.4) (2023-01-14) 14 | 15 | ## [v0.4.3](https://github.com/alenrajsp/tcxreader/tree/v0.4.3) (2023-01-14) 16 | 17 | ## [v0.4.2](https://github.com/alenrajsp/tcxreader/tree/v0.4.2) (2022-10-04) 18 | 19 | ## [v0.4.1](https://github.com/alenrajsp/tcxreader/tree/v0.4.1) (2022-08-08) 20 | 21 | ## [v0.4.0](https://github.com/alenrajsp/tcxreader/tree/v0.4.0) (2022-08-08) 22 | 23 | ## [v0.3.15](https://github.com/alenrajsp/tcxreader/tree/v0.3.15) (2022-07-26) 24 | 25 | ## [v0.3.10](https://github.com/alenrajsp/tcxreader/tree/v0.3.10) (2022-03-24) 26 | 27 | ## [v0.3.9](https://github.com/alenrajsp/tcxreader/tree/v0.3.9) (2022-03-23) 28 | 29 | ## [v0.3](https://github.com/alenrajsp/tcxreader/tree/v0.3) (2020-12-02) 30 | 31 | 32 | 33 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alen Rajšp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tcxreader * 2 | recursive-exclude example_data * 3 | recursive-exclude examples * 4 | recursive-exclude tests * 5 | recursive-exclude tcxreader.tests * 6 | prune */tests 7 | include LICENSE 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Reader for Garmin's TCX file format 7 |

8 | 9 |

10 | 11 | PyPI Version 12 | 13 | PyPI - Python Version 14 | PyPI - Downloads 15 | 16 | Downloads 17 | 18 | 19 | tcxreader 20 | 21 |

22 | 23 |

24 | GitHub repo size 25 | 26 | GitHub license 27 | 28 | GitHub commit activity 29 | 30 | Average time to resolve an issue 31 | 32 | 33 | Percentage of issues still open 34 | 35 | 36 | All Contributors 37 | 38 |

39 | 40 |

41 | 42 | DOI 43 | 44 |

45 | 46 |

47 | 🎯 Objective • 48 | ✨ Features • 49 | 📦 Installation • 50 | 🚀 Usage • 51 | 🔍 Classes explanation • 52 | 🚨 Missing data handling • 53 | 💾 Datasets • 54 | 🔗 Related packages/frameworks • 55 | 🔑 License • 56 | 🫂 Contributors 57 |

58 | 59 | ## 🎯 Objective 60 | 61 | This is a simple TCX reader which can read Garmin TCX file extension files. The following data is currently parsed: longitude, 62 | latitude, elevation, time, distance, hr_value, cadence, watts, TPX_speed (extension). The following statistics are 63 | calculated for each exercise: calories, hr_avg, hr_max, hr_min, avg_speed, start_time, end_time, duration, cadence_avg, 64 | cadence_max, ascent, descent, distance, altitude_max, altitude_min, altitude_avg, steps and **author data**. 65 | 66 | GitHub requests appreciated. 67 | [pypi](https://pypi.org/project/tcxreader/) 68 | [github](https://github.com/alenrajsp/tcxreader) 69 | 70 | ## ✨ Features 71 | 72 | Allows parsing / reading of TCX files. 73 | 74 | ## 📦 Installation 75 | 76 | ``` 77 | pip install tcxreader 78 | ``` 79 | 80 | ## 🚀 Usage 81 | 82 | An example on how to use the package is shown below. 83 | 84 | ```python 85 | from tcxreader.tcxreader import TCXReader, TCXExercise 86 | 87 | tcx_reader = TCXReader() 88 | file_location = 'example_data/cross-country-skiing_activity_1.tcx' 89 | 90 | """ 91 | Minor warning, the read method also has a default parameter of only_gps (tcx_readerread(self, fileLocation: str, only_gps: bool = True)) set to true. If set to True erases any Trackpoints at the start and end of the exercise without GPS data. 92 | """ 93 | 94 | data: TCXExercise = tcx_reader.read(file_location) 95 | """ Example output: 96 | data = {TCXExercise} 97 | activity_type = {str} 'Other' 98 | altitude_avg = {float} 2285.6744874553915 99 | altitude_max = {float} 2337.60009765625 100 | altitude_min = {float} 1257.5999755859375 101 | ascent = {float} 1117.9996337890625 102 | author = {TCXAuthor} [TCXAuthor] 103 | avg_speed = {float} 8.534458975426906 104 | cadence_avg = {NoneType} None 105 | cadence_max = {NoneType} None 106 | calories = {int} 532 107 | descent = {float} 118.19970703125 108 | distance = {float} 5692.01 109 | duration = {float} 2401.0 110 | end_time = {datetime} 2020-12-26 15:54:22 111 | hr_avg = {float} 141.1954732510288 112 | hr_max = {int} 172 113 | hr_min = {int} 83 114 | laps = {list: 2} [TCXLap] 115 | lx_ext = {dict: 0} {} 116 | max_speed = {float} 23.50810546875 117 | start_time = {datetime} 2020-12-26 15:14:21 118 | tpx_ext_stats = {dict: 2} {'Speed': {'min': 0.0, 'max': 6.1579999923706055, 'avg': 2.2930514418784482}, 'RunCadence': {'min': 0, 'max': 95, 'avg': 40.81069958847737}} 119 | trackpoints = {list: 486} [TCXTrackpoint] 120 | 121 | {TCXTrackPoint} 122 | cadence = {NoneType} None 123 | distance = {float} 7.329999923706055 124 | elevation = {float} 2250.60009765625 125 | hr_value = {int} 87 126 | latitude = {float} 46.49582446552813 127 | longitude = {float} 15.50408081151545 128 | time = {datetime} 2020-12-26 15:14:28 129 | tpx_ext = {dict: 2} {'Speed': 0.7459999918937683, 'RunCadence': 58} 130 | """ 131 | ``` 132 | ## 🔍 Classes explanation 133 | 134 | Below figure explains the classes of **tcxreader** and the data they contain. 135 | 136 | ### TCXReader() 137 | User initializes the tcxreader by creating a **TCXReader** class instance. To read the data of a **TCX activity** the user must use 138 | **TCXReader.read(*filename*)** method. 139 | The output of **read()** is an instance of **TCXExercise** class. 140 | 141 | ### TCXExercise 142 | Primary class that holds cumulative data of an exercise. TCXExercise contains **all** the **trackpoints** of an activity 143 | (e.g. from all the laps merged). 144 | 145 | ### TCXLap 146 | One TCX activity may contain multiple laps. In the TCX file they are visible by the **Lap** tag. 147 | ```xml 148 | 149 | ... 150 | 151 | ``` 152 | TCXLap contains all the trackpoints of a lap. 153 | 154 | ### TCXTrackpoint 155 | A point in an exercise. Almost always has **latitude, longitude, time**. Can also have *cadence, distance, elevation, hr_value, tpx_ext*. 156 | The tpx_ext refers to individual extensions contained inside the trackpoint. An example of the Trackpoint (pre-parsing) 157 | in the TCX file is shown below. 158 | 159 | ```xml 160 | 161 | 162 | 163 | 46.49732105433941 164 | 15.496849408373237 165 | 166 | 2277.39990234375 167 | 5001.52978515625 168 | 169 | 148 170 | 171 | 172 | 173 | 3.3589999675750732 174 | 61 175 | 176 | 177 | 178 | ``` 179 | 180 | ### tpx_ext 181 | The data parsed from the **trackpoint TPX Extensions**. Example of data (pre-parsing) is shown below. 182 | ```xml 183 | 184 | 185 | 3.3589999675750732 186 | 61 187 | 188 | 189 | ``` 190 | Can occur **once (1x)** in every **trackpoint**. 191 | ### tpx_ext_stats 192 | Contains **minimum**, **maximum** and **average** values of the recorded **tpx_ext** key. 193 | 194 | ### lx_ext 195 | The data parsed from the **lap LX Extensions**. Example of data (pre-parsing) is shown below. 196 | ```xml 197 | 198 | 199 | 1.0820000171661377 200 | 65 201 | 202 | 203 | ``` 204 | Can occur **once (1x)** in every **lap**. 205 | 206 | The tags which do not contain **Avg, Min, Max** in their name (e.g. steps) are 207 | summed in the **TCXExercise** **lx_ext** dictionary. 208 | 209 | All tags are recorded in the **TCXLap** **lx_ext** dictionary 210 | 211 | 212 | ### Schema of the data 213 |
214 | 215 |
216 | 217 | ## 🚨 Missing data handling 218 | Due to the nature of the TCX file format, some data may be missing. The **tcxreader** can handle this in two ways: 219 | 1) If data is missing at a TCX point it is set to **None**. (*default*) 220 | - tcx_reader.read(file_location) (*default*) 221 | - tcx_reader.read(file_location, null_value_handling=1) (*default*) 222 | - tcx_reader.read(file_location, null_value_handling=NullValueHandling.NONE) (*default*) 223 | 2) If data is missing at one or more TCX points it is linearly interpolated. 224 | - tcx_reader.read(file_location, null_value_handling=2) 225 | - tcx_reader.read(file_location, null_value_handling=NullValueHandling.LINEAR_INTERPOLATION) 226 | 227 | This behavior can be set in **TCXReader.read()** method by the **null_value_handling** parameter, where either **int** value or **NullValueHandling** enum can be passed. 228 | 229 | ## 💾 Datasets 230 | 231 | Datasets available and used in the examples on the following links: [DATASET1](http://iztok-jr-fister.eu/static/publications/Sport5.zip), [DATASET2](http://iztok-jr-fister.eu/static/css/datasets/Sport.zip), [DATASET3](https://github.com/firefly-cpp/tcx-test-files). 232 | 233 | ## 🔗 Related packages/frameworks 234 | 235 | [1] [sport-activities-features: A minimalistic toolbox for extracting features from sports activity files written in Python](https://github.com/firefly-cpp/sport-activities-features) 236 | 237 | [2] [AST-Monitor: A wearable Raspberry Pi computer for cyclists](https://github.com/firefly-cpp/AST-Monitor) 238 | 239 | [3] [TCXReader.jl: Julia package designed for parsing TCX files](https://github.com/firefly-cpp/TCXReader.jl) 240 | 241 | ## 🔑 License 242 | 243 | This package is distributed under the MIT License. This license can be found online 244 | at [http://www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT). 245 | 246 | ## Disclaimer 247 | 248 | This framework is provided as-is, and there are no guarantees that it fits your purposes or that it is bug-free. Use it 249 | at your own risk! 250 | 251 | ## 🫂 Contributors 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 |
alenrajsp
alenrajsp

💻 🚧
fortysix2ahead
fortysix2ahead

🐛
Iztok Fister Jr.
Iztok Fister Jr.

🔣 🧑‍🏫 📦 ⚠️
johnleeming
johnleeming

🐛
rpstar
rpstar

🐛
James Robinson
James Robinson

🚧
johwiebe
johwiebe

🐛
Martin Ueding
Martin Ueding

🐛
Simon Pickering
Simon Pickering

🐛
Rich Winkler
Rich Winkler

🐛
Toon Elewaut
Toon Elewaut

💻
Tadej Lahovnik
Tadej Lahovnik

📖
276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /example_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alenrajsp/tcxreader/4b0daf5211a7848695918dd84c27ff44891d2e93/example_data/__init__.py -------------------------------------------------------------------------------- /example_data/sup_activity_1.tcx: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 2022-07-16T16:08:25.000Z 12 | 13 | 966.796 14 | 1000.0 15 | 1.4110000133514404 16 | 80 17 | 18 | 88 19 | 20 | 21 | 108 22 | 23 | Active 24 | Manual 25 | 26 | 27 | 28 | 29 | 46.63692391477525 30 | 16.16836273111403 31 | 32 | 0.75 33 | 34 | 96 35 | 36 | 37 | 38 | 0.2680000066757202 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46.63699457421899 46 | 16.168359126895666 47 | 48 | 8.609999656677246 49 | 50 | 92 51 | 52 | 53 | 54 | 0.0 55 | 56 | 57 | 58 | 59 | 60 | 61 | 46.637007063254714 62 | 16.168354768306017 63 | 64 | 10.0 65 | 66 | 89 67 | 68 | 69 | 70 | 0.6439999938011169 71 | 72 | 73 | 74 | 75 | 76 | 77 | 46.6370128467679 78 | 16.168352086097002 79 | 80 | 10.670000076293945 81 | 82 | 86 83 | 84 | 85 | 86 | 0.6340000033378601 87 | 88 | 89 | 90 | 91 | 92 | 93 | 46.63705098442733 94 | 16.16833691485226 95 | 96 | 15.079999923706055 97 | 98 | 89 99 | 100 | 101 | 102 | 0.6140000224113464 103 | 104 | 105 | 106 | 107 | 108 | 109 | 46.63705718703568 110 | 16.168339429423213 111 | 112 | 15.739999771118164 113 | 114 | 93 115 | 116 | 117 | 118 | 0.0 119 | 120 | 121 | 122 | 123 | 124 | 125 | 46.63706154562533 126 | 16.168339429423213 127 | 128 | 16.219999313354492 129 | 130 | 96 131 | 132 | 133 | 134 | 0.5910000205039978 135 | 136 | 137 | 138 | 139 | 140 | 141 | 46.63709004409611 142 | 16.168337082490325 143 | 144 | 19.389999389648438 145 | 146 | 91 147 | 148 | 149 | 150 | 0.5180000066757202 151 | 152 | 153 | 154 | 155 | 156 | 157 | 46.63709490559995 158 | 16.168336663395166 159 | 160 | 19.93000030517578 161 | 162 | 91 163 | 164 | 165 | 166 | 0.49799999594688416 167 | 168 | 169 | 170 | 171 | 172 | 173 | 46.63705475628376 174 | 16.168371196836233 175 | 176 | 23.010000228881836 177 | 178 | 94 179 | 180 | 181 | 182 | 0.0 183 | 184 | 185 | 186 | 187 | 188 | 189 | 46.63705375045538 190 | 16.16836281493306 191 | 192 | 23.530000686645508 193 | 194 | 97 195 | 196 | 197 | 198 | 0.0 199 | 200 | 201 | 202 | 203 | 204 | 205 | 46.6371547523886 206 | 16.16833155043423 207 | 208 | 40.5099983215332 209 | 210 | 100 211 | 212 | 213 | 214 | 0.9089999794960022 215 | 216 | 217 | 218 | 219 | 220 | 221 | 46.63720764219761 222 | 16.16834370419383 223 | 224 | 46.459999084472656 225 | 226 | 103 227 | 228 | 229 | 230 | 1.1690000295639038 231 | 232 | 233 | 234 | 235 | 236 | 237 | 46.63725986145437 238 | 16.16837446577847 239 | 240 | 52.77000045776367 241 | 242 | 105 243 | 244 | 245 | 246 | 1.184999942779541 247 | 248 | 249 | 250 | 251 | 252 | 253 | 46.63757518865168 254 | 16.168481670320034 255 | 256 | 89.20999908447266 257 | 258 | 102 259 | 260 | 261 | 262 | 1.2339999675750732 263 | 264 | 265 | 266 | 267 | 268 | 269 | 46.63762154057622 270 | 16.168508743867278 271 | 272 | 94.8499984741211 273 | 274 | 99 275 | 276 | 277 | 278 | 0.9670000076293945 279 | 280 | 281 | 282 | 283 | 284 | 285 | 46.6378279030323 286 | 16.168646374717355 287 | 288 | 120.66000366210938 289 | 290 | 96 291 | 292 | 293 | 294 | 0.9660000205039978 295 | 296 | 297 | 298 | 299 | 300 | 301 | 46.63803116418421 302 | 16.16921877488494 303 | 304 | 171.14999389648438 305 | 306 | 100 307 | 308 | 309 | 310 | 1.0750000476837158 311 | 312 | 313 | 314 | 315 | 316 | 317 | 46.63803242146969 318 | 16.169230258092284 319 | 320 | 172.02000427246094 321 | 322 | 103 323 | 324 | 325 | 326 | 1.065000057220459 327 | 328 | 329 | 330 | 331 | 332 | 333 | 46.63802940398455 334 | 16.169246016070247 335 | 336 | 173.25999450683594 337 | 338 | 103 339 | 340 | 341 | 342 | 1.0829999446868896 343 | 344 | 345 | 346 | 347 | 348 | 349 | 46.63801691494882 350 | 16.16934877820313 351 | 352 | 181.27999877929688 353 | 354 | 100 355 | 356 | 357 | 358 | 1.0490000247955322 359 | 360 | 361 | 362 | 363 | 364 | 365 | 46.638010796159506 366 | 16.169397393241525 367 | 368 | 185.05999755859375 369 | 370 | 97 371 | 372 | 373 | 374 | 0.9860000014305115 375 | 376 | 377 | 378 | 379 | 380 | 381 | 46.638008281588554 382 | 16.169452546164393 383 | 384 | 189.32000732421875 385 | 386 | 94 387 | 388 | 389 | 390 | 0.921999990940094 391 | 392 | 393 | 394 | 395 | 396 | 397 | 46.63801230490208 398 | 16.169494288042188 399 | 400 | 192.5399932861328 401 | 402 | 91 403 | 404 | 405 | 406 | 0.8629999756813049 407 | 408 | 409 | 410 | 411 | 412 | 413 | 46.638005934655666 414 | 16.169966524466872 415 | 416 | 229.6300048828125 417 | 418 | 90 419 | 420 | 421 | 422 | 0.9509999752044678 423 | 424 | 425 | 426 | 427 | 428 | 429 | 46.63802278228104 430 | 16.170248156413436 431 | 432 | 251.33999633789062 433 | 434 | 84 435 | 436 | 437 | 438 | 1.13100004196167 439 | 440 | 441 | 442 | 443 | 444 | 445 | 46.63801523856819 446 | 16.170287551358342 447 | 448 | 254.47000122070312 449 | 450 | 91 451 | 452 | 453 | 454 | 1.0989999771118164 455 | 456 | 457 | 458 | 459 | 460 | 461 | 46.638010293245316 462 | 16.17029911838472 463 | 464 | 255.47000122070312 465 | 466 | 94 467 | 468 | 469 | 470 | 1.097000002861023 471 | 472 | 473 | 474 | 475 | 476 | 477 | 46.638006856665015 478 | 16.17031085304916 479 | 480 | 256.44000244140625 481 | 482 | 99 483 | 484 | 485 | 486 | 1.0800000429153442 487 | 488 | 489 | 490 | 491 | 492 | 493 | 46.638007778674364 494 | 16.1703207436949 495 | 496 | 257.1700134277344 497 | 498 | 103 499 | 500 | 501 | 502 | 1.059999942779541 503 | 504 | 505 | 506 | 507 | 508 | 509 | 46.638005431741476 510 | 16.17033583112061 511 | 512 | 258.3500061035156 513 | 514 | 106 515 | 516 | 517 | 518 | 1.062999963760376 519 | 520 | 521 | 522 | 523 | 524 | 525 | 46.637998558580875 526 | 16.17038805037737 527 | 528 | 262.42999267578125 529 | 530 | 103 531 | 532 | 533 | 534 | 1.0329999923706055 535 | 536 | 537 | 538 | 539 | 540 | 541 | 46.63799629546702 542 | 16.17040137760341 543 | 544 | 263.4800109863281 545 | 546 | 103 547 | 548 | 549 | 550 | 1.0230000019073486 551 | 552 | 553 | 554 | 555 | 556 | 557 | 46.63799579255283 558 | 16.170439934358 559 | 560 | 266.45001220703125 561 | 562 | 100 563 | 564 | 565 | 566 | 1.0089999437332153 567 | 568 | 569 | 570 | 571 | 572 | 573 | 46.63799763657153 574 | 16.17048586718738 575 | 576 | 269.9800109863281 577 | 578 | 97 579 | 580 | 581 | 582 | 0.9750000238418579 583 | 584 | 585 | 586 | 587 | 588 | 589 | 46.63800367154181 590 | 16.170509168878198 591 | 592 | 271.8399963378906 593 | 594 | 92 595 | 596 | 597 | 598 | 0.9570000171661377 599 | 600 | 601 | 602 | 603 | 604 | 605 | 46.638006856665015 606 | 16.170523837208748 607 | 608 | 273.010009765625 609 | 610 | 88 611 | 612 | 613 | 614 | 0.9639999866485596 615 | 616 | 617 | 618 | 619 | 620 | 621 | 46.638010293245316 622 | 16.170536912977695 623 | 624 | 274.07000732421875 625 | 626 | 88 627 | 628 | 629 | 630 | 0.9789999723434448 631 | 632 | 633 | 634 | 635 | 636 | 637 | 46.63803292438388 638 | 16.17064956575632 639 | 640 | 283.05999755859375 641 | 642 | 85 643 | 644 | 645 | 646 | 1.1009999513626099 647 | 648 | 649 | 650 | 651 | 652 | 653 | 46.63802077062428 654 | 16.17078778333962 655 | 656 | 293.760009765625 657 | 658 | 82 659 | 660 | 661 | 662 | 1.1859999895095825 663 | 664 | 665 | 666 | 667 | 668 | 669 | 46.637990260496736 670 | 16.170980902388692 671 | 672 | 308.95001220703125 673 | 674 | 85 675 | 676 | 677 | 678 | 1.1710000038146973 679 | 680 | 681 | 682 | 683 | 684 | 685 | 46.638022027909756 686 | 16.171511225402355 687 | 688 | 351.0199890136719 689 | 690 | 81 691 | 692 | 693 | 694 | 1.0920000076293945 695 | 696 | 697 | 698 | 699 | 700 | 701 | 46.63800023496151 702 | 16.17157844826579 703 | 704 | 356.7699890136719 705 | 706 | 84 707 | 708 | 709 | 710 | 0.906000018119812 711 | 712 | 713 | 714 | 715 | 716 | 717 | 46.63799009285867 718 | 16.171606024727225 719 | 720 | 359.1700134277344 721 | 722 | 87 723 | 724 | 725 | 726 | 0.8519999980926514 727 | 728 | 729 | 730 | 731 | 732 | 733 | 46.63798456080258 734 | 16.17175865918398 735 | 736 | 371.05999755859375 737 | 738 | 90 739 | 740 | 741 | 742 | 0.7649999856948853 743 | 744 | 745 | 746 | 747 | 748 | 749 | 46.63807793520391 750 | 16.171967117115855 751 | 752 | 390.0899963378906 753 | 754 | 90 755 | 756 | 757 | 758 | 0.9129999876022339 759 | 760 | 761 | 762 | 763 | 764 | 765 | 46.63810132071376 766 | 16.17261135019362 767 | 768 | 440.4800109863281 769 | 770 | 88 771 | 772 | 773 | 774 | 1.0420000553131104 775 | 776 | 777 | 778 | 779 | 780 | 781 | 46.63822688162327 782 | 16.17295584641397 783 | 784 | 474.25 785 | 786 | 86 787 | 788 | 789 | 790 | 0.9100000262260437 791 | 792 | 793 | 794 | 795 | 796 | 797 | 46.638283878564835 798 | 16.172961462289095 799 | 800 | 480.5299987792969 801 | 802 | 88 803 | 804 | 805 | 806 | 1.0169999599456787 807 | 808 | 809 | 810 | 811 | 812 | 813 | 46.6383149754256 814 | 16.17295299656689 815 | 816 | 484.0 817 | 818 | 89 819 | 820 | 821 | 822 | 1.0820000171661377 823 | 824 | 825 | 826 | 827 | 828 | 829 | 46.63833643309772 830 | 16.172942351549864 831 | 832 | 486.4800109863281 833 | 834 | 90 835 | 836 | 837 | 838 | 1.1319999694824219 839 | 840 | 841 | 842 | 843 | 844 | 845 | 46.63839996792376 846 | 16.172900777310133 847 | 848 | 494.1700134277344 849 | 850 | 90 851 | 852 | 853 | 854 | 1.2410000562667847 855 | 856 | 857 | 858 | 859 | 860 | 861 | 46.63846056908369 862 | 16.17284294217825 863 | 864 | 502.1400146484375 865 | 866 | 89 867 | 868 | 869 | 870 | 1.3220000267028809 871 | 872 | 873 | 874 | 875 | 876 | 877 | 46.638511111959815 878 | 16.172764571383595 879 | 880 | 510.3699951171875 881 | 882 | 88 883 | 884 | 885 | 886 | 1.3660000562667847 887 | 888 | 889 | 890 | 891 | 892 | 893 | 46.63854690268636 894 | 16.172694079577923 895 | 896 | 517.030029296875 897 | 898 | 87 899 | 900 | 901 | 902 | 1.3550000190734863 903 | 904 | 905 | 906 | 907 | 908 | 909 | 46.638559056445956 910 | 16.17266465909779 911 | 912 | 519.5700073242188 913 | 914 | 87 915 | 916 | 917 | 918 | 1.3459999561309814 919 | 920 | 921 | 922 | 923 | 924 | 925 | 46.638569785282016 926 | 16.172633981332183 927 | 928 | 522.1400146484375 929 | 930 | 87 931 | 932 | 933 | 934 | 1.347000002861023 935 | 936 | 937 | 938 | 939 | 940 | 941 | 46.63868176750839 942 | 16.17237632162869 943 | 944 | 545.719970703125 945 | 946 | 86 947 | 948 | 949 | 950 | 1.3329999446868896 951 | 952 | 953 | 954 | 955 | 956 | 957 | 46.63872535340488 958 | 16.172223687171936 959 | 960 | 558.3699951171875 961 | 962 | 85 963 | 964 | 965 | 966 | 1.3819999694824219 967 | 968 | 969 | 970 | 971 | 972 | 973 | 46.638731975108385 974 | 16.17217356339097 975 | 976 | 562.27001953125 977 | 978 | 85 979 | 980 | 981 | 982 | 1.3869999647140503 983 | 984 | 985 | 986 | 987 | 988 | 989 | 46.638887878507376 990 | 16.171739548444748 991 | 992 | 600.7899780273438 993 | 994 | 82 995 | 996 | 997 | 998 | 1.3389999866485596 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 46.638904474675655 1006 | 16.171718258410692 1007 | 1008 | 603.1699829101562 1009 | 1010 | 80 1011 | 1012 | 1013 | 1014 | 1.3289999961853027 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 46.63898485712707 1022 | 16.17152472026646 1023 | 1024 | 620.47998046875 1025 | 1026 | 79 1027 | 1028 | 1029 | 1030 | 1.3420000076293945 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 46.63898938335478 1038 | 16.171509129926562 1039 | 1040 | 621.75 1041 | 1042 | 76 1043 | 1044 | 1045 | 1046 | 1.3350000381469727 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 46.63918962702155 1054 | 16.1711117438972 1055 | 1056 | 659.8499755859375 1057 | 1058 | 80 1059 | 1060 | 1061 | 1062 | 1.2640000581741333 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 46.63930336944759 1070 | 16.1709494702518 1071 | 1072 | 677.9000244140625 1073 | 1074 | 77 1075 | 1076 | 1077 | 1078 | 1.312000036239624 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 46.63933211937547 1086 | 16.170911164954305 1087 | 1088 | 682.27001953125 1089 | 1090 | 76 1091 | 1092 | 1093 | 1094 | 1.2280000448226929 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 46.63934745825827 1102 | 16.17088844999671 1103 | 1104 | 684.6400146484375 1105 | 1106 | 76 1107 | 1108 | 1109 | 1110 | 1.215000033378601 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 46.63940998725593 1118 | 16.170760206878185 1119 | 1120 | 696.6099853515625 1121 | 1122 | 79 1123 | 1124 | 1125 | 1126 | 1.2130000591278076 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 46.63943404331803 1134 | 16.170687284320593 1135 | 1136 | 702.77001953125 1137 | 1138 | 79 1139 | 1140 | 1141 | 1142 | 1.1970000267028809 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 46.639449214562774 1150 | 16.17062014527619 1151 | 1152 | 708.1400146484375 1153 | 1154 | 79 1155 | 1156 | 1157 | 1158 | 1.2599999904632568 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 46.63957200944424 1166 | 16.170285623520613 1167 | 1168 | 737.1799926757812 1169 | 1170 | 79 1171 | 1172 | 1173 | 1174 | 1.0490000247955322 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 46.639578714966774 1182 | 16.170241869986057 1183 | 1184 | 740.5700073242188 1185 | 1186 | 79 1187 | 1188 | 1189 | 1190 | 1.0920000076293945 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 46.63958399556577 1198 | 16.1701965238899 1199 | 1200 | 743.969970703125 1201 | 1202 | 80 1203 | 1204 | 1205 | 1206 | 1.1239999532699585 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 46.63958734832704 1214 | 16.170168612152338 1215 | 1216 | 746.0599975585938 1217 | 1218 | 80 1219 | 1220 | 1221 | 1222 | 1.1200000047683716 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 46.639587096869946 1230 | 16.170139024034142 1231 | 1232 | 748.239990234375 1233 | 1234 | 80 1235 | 1236 | 1237 | 1238 | 1.1239999532699585 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 46.639628168195486 1246 | 16.169880107045174 1247 | 1248 | 769.0499877929688 1249 | 1250 | 83 1251 | 1252 | 1253 | 1254 | 1.3079999685287476 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 46.63968063890934 1262 | 16.169754713773727 1263 | 1264 | 780.239990234375 1265 | 1266 | 85 1267 | 1268 | 1269 | 1270 | 1.2690000534057617 1271 | 1272 | 1273 | 1274 | 1275 | 1276 | 1277 | 46.63968700915575 1278 | 16.169725712388754 1279 | 1280 | 782.52001953125 1281 | 1282 | 86 1283 | 1284 | 1285 | 1286 | 1.2410000562667847 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 46.639681393280625 1294 | 16.169593948870897 1295 | 1296 | 792.77001953125 1297 | 1298 | 83 1299 | 1300 | 1301 | 1302 | 1.1460000276565552 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 1309 | 46.63962607271969 1310 | 16.169447265565395 1311 | 1312 | 805.6199951171875 1313 | 1314 | 84 1315 | 1316 | 1317 | 1318 | 1.1770000457763672 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 46.63954594172537 1326 | 16.169302174821496 1327 | 1328 | 819.780029296875 1329 | 1330 | 84 1331 | 1332 | 1333 | 1334 | 1.093999981880188 1335 | 1336 | 1337 | 1338 | 1339 | 1340 | 1341 | 46.639373106881976 1342 | 16.168924486264586 1343 | 1344 | 854.8300170898438 1345 | 1346 | 84 1347 | 1348 | 1349 | 1350 | 1.218000054359436 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 1357 | 46.63934050127864 1358 | 16.168880313634872 1359 | 1360 | 859.7999877929688 1361 | 1362 | 84 1363 | 1364 | 1365 | 1366 | 1.2410000562667847 1367 | 1368 | 1369 | 1370 | 1371 | 1372 | 1373 | 46.63932541385293 1374 | 16.16885726340115 1375 | 1376 | 862.1900024414062 1377 | 1378 | 84 1379 | 1380 | 1381 | 1382 | 1.2319999933242798 1383 | 1384 | 1385 | 1386 | 1387 | 1388 | 1389 | 46.63915744051337 1390 | 16.16861779242754 1391 | 1392 | 888.3599853515625 1393 | 1394 | 82 1395 | 1396 | 1397 | 1398 | 1.1089999675750732 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | 1405 | 46.63909977301955 1406 | 16.1685508210212 1407 | 1408 | 896.530029296875 1409 | 1410 | 78 1411 | 1412 | 1413 | 1414 | 1.1699999570846558 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 46.639082338660955 1422 | 16.168533386662602 1423 | 1424 | 898.8400268554688 1425 | 1426 | 78 1427 | 1428 | 1429 | 1430 | 1.1710000038146973 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 46.63903799839318 1438 | 16.168492985889316 1439 | 1440 | 904.6300048828125 1441 | 1442 | 78 1443 | 1444 | 1445 | 1446 | 1.1720000505447388 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 46.63899349048734 1454 | 16.168455854058266 1455 | 1456 | 910.2000122070312 1457 | 1458 | 78 1459 | 1460 | 1461 | 1462 | 1.159000039100647 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 46.6389590408653 1470 | 16.168423919007182 1471 | 1472 | 914.3599853515625 1473 | 1474 | 78 1475 | 1476 | 1477 | 1478 | 1.1549999713897705 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1485 | 46.638871282339096 1486 | 16.168321324512362 1487 | 1488 | 926.8900146484375 1489 | 1490 | 81 1491 | 1492 | 1493 | 1494 | 1.0479999780654907 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 46.63864899426699 1502 | 16.168156284838915 1503 | 1504 | 954.77001953125 1505 | 1506 | 82 1507 | 1508 | 1509 | 1510 | 1.2089999914169312 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 46.638604989275336 1518 | 16.168145556002855 1519 | 1520 | 959.6599731445312 1521 | 1522 | 82 1523 | 1524 | 1525 | 1526 | 1.246000051498413 1527 | 1528 | 1529 | 1530 | 1531 | 1532 | 1533 | 46.638398710638285 1534 | 16.16807389073074 1535 | 1536 | 983.2999877929688 1537 | 1538 | 84 1539 | 1540 | 1541 | 1542 | 1.3919999599456787 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 46.638326458632946 1550 | 16.168085038661957 1551 | 1552 | 991.3200073242188 1553 | 1554 | 85 1555 | 1556 | 1557 | 1558 | 1.3919999599456787 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 46.63830223493278 1566 | 16.168097108602524 1567 | 1568 | 994.1199951171875 1569 | 1570 | 84 1571 | 1572 | 1573 | 1574 | 1.409000039100647 1575 | 1576 | 1577 | 1578 | 1579 | 1580 | 1581 | 46.63824205286801 1582 | 16.16813398897648 1583 | 1584 | 1001.3599853515625 1585 | 1586 | 84 1587 | 1588 | 1589 | 1590 | 1.4110000133514404 1591 | 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1.034000039100647 1598 | 426 1599 | 1600 | 1601 | 1602 | 1603 | 145.539 1604 | 157.51 1605 | 1.3969999551773071 1606 | 12 1607 | 1608 | 87 1609 | 1610 | 1611 | 93 1612 | 1613 | Active 1614 | Manual 1615 | 1616 | 1617 | 1618 | 1619 | 46.63809830322862 1620 | 16.16813700646162 1621 | 1622 | 1017.6900024414062 1623 | 1624 | 83 1625 | 1626 | 1627 | 1628 | 1.1670000553131104 1629 | 1630 | 1631 | 1632 | 1633 | 1634 | 1635 | 46.63788087666035 1636 | 16.16815938614309 1637 | 1638 | 1042.1700439453125 1639 | 1640 | 86 1641 | 1642 | 1643 | 1644 | 1.3049999475479126 1645 | 1646 | 1647 | 1648 | 1649 | 1650 | 1651 | 46.63769446313381 1652 | 16.168130049481988 1653 | 1654 | 1063.1199951171875 1655 | 1656 | 87 1657 | 1658 | 1659 | 1660 | 1.0829999446868896 1661 | 1662 | 1663 | 1664 | 1665 | 1666 | 1667 | 46.63759094662964 1668 | 16.16813650354743 1669 | 1670 | 1074.6099853515625 1671 | 1672 | 88 1673 | 1674 | 1675 | 1676 | 1.069000005722046 1677 | 1678 | 1679 | 1680 | 1681 | 1682 | 1683 | 46.637246115133166 1684 | 16.168147567659616 1685 | 1686 | 1114.5799560546875 1687 | 1688 | 85 1689 | 1690 | 1691 | 1692 | 1.3580000400543213 1693 | 1694 | 1695 | 1696 | 1697 | 1698 | 1699 | 46.63711837492883 1700 | 16.1681604757905 1701 | 1702 | 1129.1700439453125 1703 | 1704 | 92 1705 | 1706 | 1707 | 1708 | 1.1139999628067017 1709 | 1710 | 1711 | 1712 | 1713 | 1714 | 1715 | 46.63711032830179 1716 | 16.1681691929698 1717 | 1718 | 1130.280029296875 1719 | 1720 | 92 1721 | 1722 | 1723 | 1724 | 1.1009999513626099 1725 | 1726 | 1727 | 1728 | 1729 | 1730 | 1731 | 46.63706841878593 1732 | 16.168218730017543 1733 | 1734 | 1136.280029296875 1735 | 1736 | 89 1737 | 1738 | 1739 | 1740 | 1.1610000133514404 1741 | 1742 | 1743 | 1744 | 1745 | 1746 | 1747 | 46.63698686286807 1748 | 16.16829777136445 1749 | 1750 | 1147.1800537109375 1751 | 1752 | 86 1753 | 1754 | 1755 | 1756 | 1.1130000352859497 1757 | 1758 | 1759 | 1760 | 1761 | 1762 | 1763 | 46.63692316040397 1764 | 16.16832710802555 1765 | 1766 | 1154.6099853515625 1767 | 1768 | 90 1769 | 1770 | 1771 | 1772 | 0.7099999785423279 1773 | 1774 | 1775 | 1776 | 1777 | 1778 | 1779 | 46.63691896945238 1780 | 16.168327778577805 1781 | 1782 | 1155.0799560546875 1783 | 1784 | 93 1785 | 1786 | 1787 | 1788 | 0.6710000038146973 1789 | 1790 | 1791 | 1792 | 1793 | 1794 | 1795 | 46.63691394031048 1796 | 16.168330293148756 1797 | 1798 | 1155.6700439453125 1799 | 1800 | 93 1801 | 1802 | 1803 | 1804 | 0.6470000147819519 1805 | 1806 | 1807 | 1808 | 1809 | 1810 | 1811 | 46.63689751178026 1812 | 16.168332304805517 1813 | 1814 | 1157.510009765625 1815 | 1816 | 92 1817 | 1818 | 1819 | 1820 | 0.0 1821 | 1822 | 1823 | 1824 | 1825 | 1826 | 1827 | 1.0820000171661377 1828 | 65 1829 | 1830 | 1831 | 1832 | 1833 | vívoactive HR 1834 | 3937453043 1835 | 2337 1836 | 1837 | 5 1838 | 30 1839 | 0 1840 | 0 1841 | 1842 | 1843 | 1844 | 1845 | 1846 | Connect Api 1847 | 1848 | 1849 | 0 1850 | 0 1851 | 0 1852 | 0 1853 | 1854 | 1855 | en 1856 | 006-D2449-00 1857 | 1858 | 1859 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alenrajsp/tcxreader/4b0daf5211a7848695918dd84c27ff44891d2e93/examples/__init__.py -------------------------------------------------------------------------------- /examples/reader_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example of using the TCX reader! 3 | """ 4 | from tcxreader.tcxreader import TCXReader, TCXExercise 5 | tcx_reader = TCXReader() 6 | file_location = './example_data/15.tcx' 7 | 8 | data: TCXExercise = tcx_reader.read(file_location) 9 | 10 | 11 | print("Output") 12 | print(str(data.trackpoints[0])) 13 | 14 | """ 15 | Example output: 16 | 17 | = {TCXTrackPoint} 18 | Time:2015-02-19 09:31:29 19 | Latitude: 45.524007584899664 20 | Longitude: 13.59806134365499 21 | Elevation: 19.399999618530273 22 | Distance: 0.009999999776482582 23 | Heartrate: 94 24 | Cadence: None 25 | TPX Extensions: 26 | Speed:0.0 27 | ############################# 28 | """ 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |

Placeholder for Tcxreader page.

2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tcxreader" 3 | version = "0.4.11" 4 | description = "tcxreader is a reader for Garmin’s TCX file format. It also works well with missing data!" 5 | authors = ["Alen Rajšp "] 6 | license = "MIT" 7 | homepage = "https://github.com/alenrajsp/tcxreader" 8 | repository = "https://github.com/alenrajsp/tcxreader" 9 | classifiers = [ 10 | "Programming Language :: Python :: 3.6", 11 | "License :: OSI Approved :: MIT License", 12 | "Operating System :: OS Independent", 13 | "Development Status :: 4 - Beta" 14 | ] 15 | packages = [ 16 | {include = "tcxreader"} 17 | ] 18 | 19 | include = [ 20 | { path="LICENSE", format="sdist" }, 21 | { path="README.md", format="sdist" } 22 | ] 23 | 24 | exclude= ["tests", "examples", "tcxreader/tests", "development"] 25 | 26 | [tool.poetry.dependencies] 27 | python = "^3.6" 28 | 29 | [tool.poetry.dev-dependencies] 30 | # Add your development dependencies here (e.g., testing frameworks) 31 | 32 | [build-system] 33 | requires = ["poetry-core>=1.0.0"] 34 | build-backend = "poetry.core.masonry.api" 35 | 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # - *- coding: utf- 8 - *- 2 | import setuptools 3 | 4 | with open("README.md", encoding='UTF-8', mode="r") as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="tcxreader", 9 | version="0.4.11", 10 | author="Alen Rajšp", 11 | author_email="alen.rajsp@gmail.com", 12 | description="tcxreader is a reader for Garmin’s TCX file format. It also works well with missing data!", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/alenrajsp/tcxreader", 16 | packages=setuptools.find_packages(exclude=("*.tests", "*.tests.*", "tests.*", "tests", '*tests*', 17 | "*tcxreader.tests", "*.tcxreader.tests.*", "tcxreader.tests.*", 18 | "tcxreader.tests", '*tcxreader.tests*', 19 | "*.examples","*.examples.*", "examples.*", "examples", 20 | "*.example_data", "*.example_data.*", "example_data.*", "example_data")), 21 | classifiers=[ 22 | "Programming Language :: Python :: 3.6", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | "Development Status :: 4 - Beta", 26 | ], 27 | python_requires='>=3.6', 28 | test_suite="tests" 29 | ) 30 | -------------------------------------------------------------------------------- /tcxreader/__init__.py: -------------------------------------------------------------------------------- 1 | from .tcxreader import TCXReader 2 | from .tcx_track_point import TCXTrackPoint 3 | from .tcx_author import TCXAuthor 4 | from .tcx_exercise import TCXExercise 5 | from .tcx_lap import TCXLap 6 | 7 | __all__ = [TCXReader, TCXTrackPoint, TCXAuthor, TCXExercise, TCXLap] 8 | -------------------------------------------------------------------------------- /tcxreader/tcx_author.py: -------------------------------------------------------------------------------- 1 | class TCXAuthor: 2 | def __init__(self, name: str = None, version_major: int = None, version_minor: int = None, build_major: int = None, 3 | build_minor: int = None): 4 | """ 5 | Class for storing the author of the TCX file. This is usually the device used for recording. 6 | :param name: Name of the device used for recording (usually). 7 | :param version_major: Major (XX) version of the device software (e.g. v XX.12). 8 | :param version_minor: Minor (YY) version of the device software (e.g. v 12.YY). 9 | :param build_major: Major version of the build of the device software. 10 | :param build_minor: Minor version of the build of the device software. 11 | """ 12 | self.name: str = name 13 | self.version_major: int = version_major 14 | self.version_minor: int = version_minor 15 | self.build_major: int = build_major 16 | self.build_minor: int = build_minor 17 | -------------------------------------------------------------------------------- /tcxreader/tcx_exercise.py: -------------------------------------------------------------------------------- 1 | from tcxreader.tcx_author import TCXAuthor 2 | from tcxreader.tcx_lap import TCXLap 3 | from tcxreader.tcx_track_point import TCXTrackPoint 4 | from datetime import datetime 5 | from typing import List 6 | 7 | 8 | class TCXExercise: 9 | def __init__(self, trackpoints: List[TCXTrackPoint] = None, activity_type: str = None, calories: int = None, 10 | hr_avg: float = None, hr_max: float = None, hr_min=None, max_speed: float = None, 11 | avg_speed: float = None, start_time: datetime = None, end_time: datetime = None, 12 | duration: float = None, cadence_avg: float = None, cadence_max: float = None, ascent: float = None, 13 | descent: float = None, distance: float = None, altitude_avg: float = None, altitude_min: float = None, 14 | altitude_max: float = None, author: TCXAuthor=None, 15 | tpx_ext_stats: dict = None, lx_ext: dict = None, laps: List[TCXLap] = None): 16 | """ 17 | Class for storing exercise data from a TCX file. 18 | :param trackpoints: List of TCXTrackPoint objects. 19 | :param activity_type: Sport string e.g. cycling. 20 | :param calories: Total calories used in an exercise. 21 | :param hr_avg: Maximum heartrate achieved during the exercise. 22 | :param hr_max: Average heartrate during the exercise. 23 | :param hr_min: Minimum heartrate achieved during the exercise. 24 | :param avg_speed: Average speed during the exercise (km/h). 25 | :param start_time: Datetime of exercise start. 26 | :param end_time: Datetime of exercise end. 27 | :param duration: Duration of exercise in seconds. 28 | :param cadence_avg: Average cadence during the exercise. 29 | :param cadence_max: Maximum cadence during the exercise. 30 | :param ascent: Total meters of ascent during the exercise. 31 | :param descent: Total meters of descent during the exercise. 32 | :param distance: Total distance of exercise in meters. 33 | :param altitude_avg: Average altitude in meters. 34 | :param altitude_min: Minimum altitude during the exercise. 35 | :param altitude_max: Maximum altitude during the exersice. 36 | :param author: Describes who recorded the data, e.g. which device. 37 | :param tpx_ext_stats: Contains statistics (min, max, avg) of TPX extension data of trackpoints. 38 | :param lx_ext: Contains sum of LX extension data for each key in LX extension tag. 39 | :param laps: Contains subset of data for each lap. 40 | 41 | """ 42 | 43 | self.trackpoints: List[TCXTrackPoint] = trackpoints 44 | self.laps: List[TCXLap] = laps 45 | self.activity_type: str = activity_type 46 | self.calories: int = calories 47 | self.hr_avg: float = hr_avg 48 | self.hr_max: float = hr_max 49 | self.hr_min: float = hr_min 50 | self.duration: float = duration 51 | self.max_speed: float = max_speed 52 | self.avg_speed: float = avg_speed 53 | self.start_time: datetime = start_time 54 | self.end_time: datetime = end_time 55 | self.cadence_avg: float = cadence_avg 56 | self.cadence_max: float = cadence_max 57 | self.ascent: float = ascent 58 | self.descent: float = descent 59 | self.distance: float = distance 60 | self.altitude_avg: float = altitude_avg 61 | self.altitude_min: float = altitude_min 62 | self.altitude_max: float = altitude_max 63 | self.author: TCXAuthor = author 64 | self.tpx_ext_stats: dict = tpx_ext_stats 65 | self.lx_ext: dict = lx_ext 66 | 67 | def trackpoints_to_dict(self) -> list: 68 | """ 69 | Convert trackpoints to a list of dictionaries. 70 | :return: list: A list of dictionaries containing trackpoint data. 71 | """ 72 | trackpoint_dict = [] 73 | 74 | for tp in self.trackpoints: 75 | trackpoint_dict.append(tp.to_dict()) 76 | 77 | 78 | return trackpoint_dict 79 | -------------------------------------------------------------------------------- /tcxreader/tcx_lap.py: -------------------------------------------------------------------------------- 1 | from tcxreader.tcx_track_point import TCXTrackPoint 2 | from datetime import datetime 3 | from typing import List 4 | 5 | 6 | class TCXLap: 7 | def __init__(self, trackpoints: List[TCXTrackPoint] = None, calories: int = None, 8 | hr_avg: float = None, hr_max: int = None, hr_min: int = None, max_speed: float = None, 9 | avg_speed: float = None, start_time: datetime = None, end_time: datetime = None, 10 | duration: float = None, cadence_avg: float = None, cadence_max: float = None, ascent: float = None, 11 | descent: float = None, distance: float = None, altitude_avg: float = None, altitude_min: float = None, 12 | altitude_max: float = None, lx_ext: dict = None, tpx_ext_stats: dict = None, 13 | ): 14 | """ 15 | Similar to TCXExercise, but is a container class for a lap. 16 | :param tpx_ext_stats: Contains statistics (min, max, avg) of TPX extension data of trackpoints. 17 | :param lx_ext: Contains LX extension data. 18 | :param trackpoints: List of TCXTrackPoint objects. 19 | :param calories: Total calories used in an exercise. 20 | :param hr_avg: Maximum heartrate achieved during the exercise. 21 | :param hr_max: Average heartrate during the exercise. 22 | :param hr_min: Minimum heartrate achieved during the exercise. 23 | :param avg_speed: Average speed during the exercise (km/h). 24 | :param start_time: Datetime of exercise start. 25 | :param end_time: Datetime of exercise end. 26 | :param duration: Duration of exercise in seconds. 27 | :param cadence_avg: Average cadence during the exercise. 28 | :param cadence_max: Maximum cadence during the exercise. 29 | :param ascent: Total meters of ascent during the exercise. 30 | :param descent: Total meters of descent during the exercise. 31 | :param distance: Total distance of exercise in meters. 32 | :param altitude_avg: Average altitude in meters. 33 | :param altitude_min: Minimum altitude during the exercise. 34 | :param altitude_max: Maximum altitude during the exercise. 35 | 36 | """ 37 | 38 | self.trackpoints: List[TCXTrackPoint] = trackpoints 39 | self.calories: int = calories 40 | self.hr_avg: float = hr_avg 41 | self.hr_max: int = hr_max 42 | self.hr_min: int = hr_min 43 | self.duration = duration 44 | self.max_speed: float = max_speed 45 | self.avg_speed: float = avg_speed 46 | self.start_time: datetime = start_time 47 | self.end_time: datetime = end_time 48 | self.cadence_avg: float = cadence_avg 49 | self.cadence_max: float = cadence_max 50 | self.ascent: float = ascent 51 | self.descent: float = descent 52 | self.distance: float = distance 53 | self.altitude_avg: float = altitude_avg 54 | self.altitude_min: float = altitude_min 55 | self.altitude_max: float = altitude_max 56 | self.tpx_ext_stats: dict = tpx_ext_stats 57 | if self.tpx_ext_stats == None: 58 | self.tpx_ext_stats: dict = {} 59 | self.lx_ext: dict = lx_ext 60 | if self.lx_ext == None: 61 | self.lx_ext: dict = {} 62 | -------------------------------------------------------------------------------- /tcxreader/tcx_track_point.py: -------------------------------------------------------------------------------- 1 | class TCXTrackPoint(object): 2 | def __init__(self, longitude: float = None, latitude: float = None, elevation: float = None, time=None, 3 | distance=None, hr_value: int = None, cadence=None, tpx_ext: dict = {}): 4 | """ 5 | Class for storing individual trackpoints from a TCX file. 6 | :param longitude: Longitude of the trackpoint. 7 | :param latitude: Latitude of the trackpoint. 8 | :param elevation: Elevation of the trackpoint. 9 | :param time: Datetime of the trackpoint. 10 | :param distance: Total distance traveled at the current trackpoint. 11 | :param hr_value: Heart rate value at the trackpoint. 12 | :param cadence: Cadence at the trackpoint. 13 | :param tpx_ext: Dictionary of all additional data types! e,g, speed, cadence, runcadence. 14 | """ 15 | self.longitude: float = longitude 16 | self.latitude: float = latitude 17 | self.elevation: float = elevation 18 | self.time = time 19 | self.distance: float = distance 20 | self.hr_value: int = hr_value 21 | self.cadence: int = cadence 22 | self.tpx_ext: dict = tpx_ext 23 | 24 | def __str__(self) -> str: 25 | (longitude, latitude, elevation) = (self.longitude, self.latitude, self.elevation) 26 | time = self.time 27 | distance = self.distance 28 | (hr_value, cadence) = (self.hr_value, self.cadence) 29 | 30 | tpx_str = "" 31 | for key in self.tpx_ext: 32 | tpx_str += f'\n\t{key}:{self.tpx_ext[key]}' 33 | 34 | return f'Time:{time}\nLatitude:\t{latitude}\nLongitude:\t{longitude}\nElevation:\t{elevation}\nDistance:\t{distance}\n' \ 35 | f'Heartrate:\t{hr_value}\nCadence:\t{cadence} \nTPX Extensions: {tpx_str}\n#############################' 36 | 37 | def to_dict(self) -> dict: 38 | """ 39 | Convert trackpoint (TCXTrackPoint) to a dictionary. 40 | :return: A dictionary containing trackpoint data. 41 | """ 42 | tp_dict = { 43 | 'time': self.time, 44 | 'longitude': self.longitude, 45 | 'latitude': self.latitude, 46 | 'distance': self.distance, 47 | 'elevation': self.elevation, 48 | 'hr_value': self.hr_value, 49 | 'cadence': self.cadence, 50 | } 51 | for key in self.tpx_ext: 52 | tp_dict[key] = self.tpx_ext[key] 53 | 54 | return tp_dict 55 | 56 | def __unicode__(self): 57 | return self.__str__() 58 | -------------------------------------------------------------------------------- /tcxreader/tcxreader.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import xml.etree.ElementTree as ET 3 | from enum import Enum 4 | from typing import List, Union 5 | 6 | from tcxreader.tcx_author import TCXAuthor 7 | from tcxreader.tcx_exercise import TCXExercise 8 | from tcxreader.tcx_lap import TCXLap 9 | from tcxreader.tcx_track_point import TCXTrackPoint 10 | 11 | GARMIN_XML_SCHEMA = '{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}' 12 | GARMIN_XML_EXTENSIONS = '{http://www.garmin.com/xmlschemas/ActivityExtension/v2}' 13 | 14 | 15 | class NullValueHandling(Enum): 16 | """ 17 | Enum for handling null values in TCX file. 18 | """ 19 | NONE = 1 20 | LINEAR_INTERPOLATION = 2 21 | 22 | 23 | class TCXReader: 24 | def __init__(self): 25 | """ 26 | Class for reading TCX files. 27 | """ 28 | pass 29 | 30 | def read(self, fileLocation: str, only_gps: bool = True, null_value_handling: int = 1) -> TCXExercise: 31 | """ 32 | Reads a TCX file and returns a TCXExercise object. 33 | 34 | :param fileLocation: Path to the TCX file. 35 | :param only_gps: If True, remove any Trackpoints at the start/end of the exercise without GPS data. 36 | :param null_value_handling: How to handle null values: 37 | 1 = set to None 38 | 2 = linear interpolation 39 | :return: A TCXExercise object. 40 | """ 41 | # 1) Build an empty TCXExercise container 42 | tcx_exercise = TCXExercise(calories=0, distance=0, tpx_ext_stats={}, lx_ext={}, laps=[]) 43 | 44 | # 2) Parse the file into a tree and extract the root 45 | tree, root = self.__parse_tcx_file(fileLocation) 46 | 47 | # 3) Read all activities and populate the `tcx_exercise` data 48 | trackpoints = self.__parse_activities(root, tcx_exercise) 49 | 50 | # 4) Read the file’s author (if present) 51 | self.__parse_author(root, tcx_exercise) 52 | 53 | # 5) Remove trackpoints that do not have GPS data if only_gps is True 54 | if only_gps: 55 | self.__remove_data_at_start_and_end_without_gps(trackpoints) 56 | 57 | # 6) Store the (possibly truncated) trackpoints in the top-level exercise 58 | tcx_exercise.trackpoints = trackpoints 59 | 60 | # 7) Interpolate missing values if requested 61 | if null_value_handling == 2 or null_value_handling == NullValueHandling.LINEAR_INTERPOLATION: 62 | tcx_exercise.trackpoints = self.__fill_none_with_averages(tcx_exercise.trackpoints) 63 | 64 | # 8) Calculate additional stats (min, max, avg, etc.) at the exercise level 65 | tcx_exercise = self.__find_hi_lo_avg(tcx_exercise, only_gps) 66 | 67 | # 9) Handle laps individually (fill missing data + calculate stats) 68 | for lap in tcx_exercise.laps: 69 | if null_value_handling == 2 or null_value_handling == NullValueHandling.LINEAR_INTERPOLATION: 70 | lap.trackpoints = self.__fill_none_with_averages(lap.trackpoints) 71 | self.__find_hi_lo_avg(lap, only_gps) 72 | 73 | return tcx_exercise 74 | 75 | # -------------------------------------------------------------------------- 76 | # HELPER METHODS 77 | # -------------------------------------------------------------------------- 78 | 79 | def __parse_tcx_file(self, fileLocation: str) -> Union[ET.ElementTree, ET.Element]: 80 | """ 81 | Parses an XML file into an ElementTree and returns the tree + root. 82 | 83 | :param fileLocation: Path to the TCX file 84 | :return: (tree, root) 85 | """ 86 | tree = ET.parse(fileLocation) 87 | root = tree.getroot() 88 | return tree, root 89 | 90 | def __parse_activities(self, root: ET.Element, tcx_exercise: TCXExercise) -> List[TCXTrackPoint]: 91 | """ 92 | Reads from the root and populates laps, trackpoints, and summary 93 | data (like total distance, total calories) in the `tcx_exercise` object. 94 | 95 | :param root: The root of the parsed TCX file 96 | :param tcx_exercise: The exercise container to fill 97 | :return: A flat list of all trackpoints found in the file 98 | """ 99 | trackpoints = [] 100 | 101 | for node in root: 102 | if node.tag == GARMIN_XML_SCHEMA + 'Activities': 103 | for activity in node: 104 | if activity.tag == GARMIN_XML_SCHEMA + 'Activity': 105 | # Sport Type 106 | tcx_exercise.activity_type = activity.attrib['Sport'] 107 | 108 | # Parse elements inside an Activity 109 | for lap_node in activity: 110 | if lap_node.tag == GARMIN_XML_SCHEMA + 'Lap': 111 | tcx_lap = self.__parse_lap(lap_node, tcx_exercise) 112 | if len(tcx_lap.trackpoints) > 0: 113 | tcx_exercise.laps.append(tcx_lap) 114 | trackpoints.extend(tcx_lap.trackpoints) 115 | return trackpoints 116 | 117 | def __parse_lap(self, lap_node: ET.Element, tcx_exercise: TCXExercise) -> TCXLap: 118 | """ 119 | Parses a single element, extracting Calories, DistanceMeters, trackpoints, 120 | and any lap-level LX extensions. The resulting data is stored in a new TCXLap, 121 | which is returned. 122 | 123 | :param lap_node: The element 124 | :param tcx_exercise: The high-level TCXExercise container 125 | :return: A newly created TCXLap with the parsed data 126 | """ 127 | tcx_lap = TCXLap(calories=0, distance=0, trackpoints=[], tpx_ext_stats={}, lx_ext={}) 128 | 129 | for lap_child in lap_node: 130 | # Calories 131 | if lap_child.tag == GARMIN_XML_SCHEMA + 'Calories': 132 | calories = int(round(float(lap_child.text))) 133 | tcx_exercise.calories += calories 134 | tcx_lap.calories += calories 135 | 136 | # Distance 137 | elif lap_child.tag == GARMIN_XML_SCHEMA + 'DistanceMeters': 138 | distance_val = float(lap_child.text) 139 | tcx_exercise.distance += distance_val 140 | tcx_lap.distance += distance_val 141 | 142 | # Track (set of Trackpoint) 143 | elif lap_child.tag == GARMIN_XML_SCHEMA + 'Track': 144 | for trackpoint in lap_child: 145 | if trackpoint.tag == GARMIN_XML_SCHEMA + 'Trackpoint': 146 | tcx_point = TCXTrackPoint(tpx_ext={}) 147 | self.trackpoint_parser(tcx_point, trackpoint) 148 | tcx_lap.trackpoints.append(tcx_point) 149 | 150 | # Lap-level 151 | elif lap_child.tag == GARMIN_XML_SCHEMA + 'Extensions': 152 | for extension in lap_child: 153 | if extension.tag == GARMIN_XML_EXTENSIONS + 'LX': 154 | # Example: ...... 155 | for lx_extension in extension: 156 | tag_name = lx_extension.tag.replace(GARMIN_XML_EXTENSIONS, "") 157 | tag_value = lx_extension.text 158 | # Convert to float or int 159 | if '.' in tag_value: 160 | tag_value = float(tag_value) 161 | else: 162 | tag_value = int(tag_value) 163 | 164 | # Summation into the exercise-level dictionary 165 | if "Avg" in tag_name or "Average" in tag_name or "Max" in tag_name or "Min" in tag_name: 166 | # We skip adding these particular stats to a sum 167 | pass 168 | else: 169 | if tag_name in tcx_exercise.lx_ext: 170 | tcx_exercise.lx_ext[tag_name] += tag_value 171 | else: 172 | tcx_exercise.lx_ext[tag_name] = tag_value 173 | 174 | # Also store in the lap-level dictionary 175 | tcx_lap.lx_ext[tag_name] = tag_value 176 | 177 | return tcx_lap 178 | 179 | def __parse_author(self, root: ET.Element, tcx_exercise: TCXExercise) -> None: 180 | """ 181 | Reads the element (if present) and populates an Author object 182 | in the `tcx_exercise`. 183 | 184 | :param root: The root of the parsed TCX file 185 | :param tcx_exercise: The exercise container to fill with author info 186 | :return: None 187 | """ 188 | for node in root: 189 | if node.tag == GARMIN_XML_SCHEMA + 'Author': 190 | author = TCXAuthor() 191 | for author_node in node: 192 | if author_node.tag == GARMIN_XML_SCHEMA + 'Name': 193 | author.name = author_node.text 194 | elif author_node.tag == GARMIN_XML_SCHEMA + 'Build': 195 | for build_node in author_node: 196 | if build_node.tag == GARMIN_XML_SCHEMA + 'Version': 197 | for version_node in build_node: 198 | if version_node.tag == GARMIN_XML_SCHEMA + 'VersionMajor': 199 | author.version_major = int(version_node.text) 200 | elif version_node.tag == GARMIN_XML_SCHEMA + 'VersionMinor': 201 | author.version_minor = int(version_node.text) 202 | elif version_node.tag == GARMIN_XML_SCHEMA + 'BuildMajor': 203 | author.build_major = int(version_node.text) 204 | elif version_node.tag == GARMIN_XML_SCHEMA + 'BuildMinor': 205 | author.build_minor = int(version_node.text) 206 | tcx_exercise.author = author 207 | 208 | def __remove_data_at_start_and_end_without_gps(self, trackpoints: list) -> None: 209 | """ 210 | Removes any TrackPoint that does not have longitude data. This effectively 211 | removes trackpoints that don't contain GPS info from the start/end. 212 | 213 | :param trackpoints: A list of all trackpoints 214 | :return: None (operates in-place) 215 | """ 216 | removal_list = [] 217 | for idx, tp in enumerate(trackpoints): 218 | if tp.longitude is None: 219 | removal_list.append(idx) 220 | 221 | # remove from the end to avoid shifting indexes 222 | for removal in sorted(removal_list, reverse=True): 223 | del trackpoints[removal] 224 | 225 | def trackpoint_parser(self, tcx_point: TCXTrackPoint, trackpoint: ET.Element) -> None: 226 | """ 227 | Parses a XML element and fills the provided `tcx_point` object. 228 | 229 | :param tcx_point: TCXTrackPoint object to store parsed data 230 | :param trackpoint: element from the TCX 231 | :return: None 232 | """ 233 | for trackpoint_data in trackpoint: 234 | if trackpoint_data.tag == GARMIN_XML_SCHEMA + 'Time': 235 | # Attempt multiple time format patterns 236 | for pat in ( 237 | "%Y-%m-%dT%H:%M:%S.%fZ", 238 | "%Y-%m-%dT%H:%M:%S.%f%z", 239 | "%Y-%m-%dT%H:%M:%SZ", 240 | "%Y-%m-%dT%H:%M:%S%z" 241 | ): 242 | try: 243 | tcx_point.time = datetime.datetime.strptime(trackpoint_data.text, pat) 244 | break 245 | except ValueError: 246 | continue 247 | 248 | if not tcx_point.time: 249 | raise ValueError(f'Cannot parse time {trackpoint_data.text!r}') 250 | 251 | elif trackpoint_data.tag == GARMIN_XML_SCHEMA + 'Position': 252 | for position in trackpoint_data: 253 | if position.tag == GARMIN_XML_SCHEMA + "LatitudeDegrees": 254 | try: 255 | tcx_point.latitude = float(position.text) 256 | except (ValueError, TypeError): 257 | tcx_point.latitude = None 258 | elif position.tag == GARMIN_XML_SCHEMA + "LongitudeDegrees": 259 | try: 260 | tcx_point.longitude = float(position.text) 261 | except (ValueError, TypeError): 262 | tcx_point.longitude = None 263 | 264 | elif trackpoint_data.tag == GARMIN_XML_SCHEMA + 'AltitudeMeters': 265 | try: 266 | tcx_point.elevation = float(trackpoint_data.text) 267 | except (ValueError, TypeError): 268 | tcx_point.elevation = None 269 | 270 | elif trackpoint_data.tag == GARMIN_XML_SCHEMA + 'DistanceMeters': 271 | try: 272 | tcx_point.distance = float(trackpoint_data.text) 273 | except (ValueError, TypeError): 274 | tcx_point.distance = None 275 | 276 | elif trackpoint_data.tag == GARMIN_XML_SCHEMA + 'HeartRateBpm': 277 | for heart_rate in trackpoint_data: 278 | try: 279 | tcx_point.hr_value = int(float(heart_rate.text)) 280 | except (ValueError, TypeError): 281 | tcx_point.hr_value = None 282 | 283 | elif trackpoint_data.tag == GARMIN_XML_SCHEMA + 'Cadence': 284 | try: 285 | tcx_point.cadence = int(float(trackpoint_data.text)) 286 | except (ValueError, TypeError): 287 | tcx_point.cadence = None 288 | 289 | elif trackpoint_data.tag == GARMIN_XML_SCHEMA + 'Extensions': 290 | for extension in trackpoint_data: 291 | if extension.tag == GARMIN_XML_EXTENSIONS + 'TPX': 292 | # e.g. ...... 293 | for tpx_extension in extension: 294 | tag_name = tpx_extension.tag.replace(GARMIN_XML_EXTENSIONS, "") 295 | try: 296 | tag_value = tpx_extension.text 297 | if '.' in tag_value: 298 | tag_value = float(tag_value) 299 | else: 300 | tag_value = int(tag_value) 301 | tcx_point.tpx_ext[tag_name] = tag_value 302 | except (ValueError, TypeError): 303 | tcx_point.tpx_ext[tag_name] = None 304 | 305 | def __fill_none_with_averages(self, trackpoints: List[TCXTrackPoint]) -> List[TCXTrackPoint]: 306 | """ 307 | Interpolates missing values in trackpoints with averages between the 308 | previous and next valid value. 309 | 310 | :param trackpoints: List of TCXTrackPoint objects 311 | :return: A new list of TCXTrackPoint with missing values linearly interpolated 312 | """ 313 | def interpolate(start, end, length): 314 | """ 315 | Interpolates between two values. 316 | :param start: 317 | :param end: 318 | :param length: 319 | :return: 320 | """ 321 | step = (end - start) / (length + 1) if length + 1 != 0 else 0 322 | type_start = type(start) 323 | return [type_start(start + step * i) for i in range(1, length + 1)] 324 | 325 | def __fill_none_with_averages_for_data(data): 326 | """ 327 | Interpolates missing values in data with averages between them. 328 | :param data: 329 | :return: 330 | """ 331 | result = [] 332 | i = 0 333 | while i < len(data): 334 | if data[i] is None: 335 | start_i = i - 1 336 | while i < len(data) and data[i] is None: 337 | i += 1 338 | end_i = i 339 | start_val = data[start_i] if start_i >= 0 else 0 340 | end_val = data[end_i] if end_i < len(data) else start_val 341 | result.extend(interpolate(start_val, end_val, end_i - start_i)) 342 | else: 343 | result.append(data[i]) 344 | i += 1 345 | return result 346 | 347 | # Interpolate for simple numeric attributes 348 | def interpolate_attribute(attr): 349 | """ 350 | Interpolates an attribute. 351 | :param attr: 352 | :return: 353 | """ 354 | data = [getattr(tp, attr) for tp in trackpoints] 355 | return __fill_none_with_averages_for_data(data) 356 | 357 | # Interpolate for each known extension key 358 | def interpolate_tpx_ext(key): 359 | """ 360 | Interpolates a TPX extension key. 361 | :param key: 362 | :return: 363 | """ 364 | data = [tp.tpx_ext.get(key) for tp in trackpoints] 365 | return __fill_none_with_averages_for_data(data) 366 | 367 | attrs_to_interpolate = ['longitude', 'latitude', 'elevation', 'distance', 'hr_value', 'cadence'] 368 | interpolated_attrs = {attr: interpolate_attribute(attr) for attr in attrs_to_interpolate} 369 | 370 | # Collect all extension keys 371 | tpx_keys = set() 372 | for tp in trackpoints: 373 | tpx_keys.update(tp.tpx_ext.keys()) 374 | 375 | interpolated_tpx_ext = {key: interpolate_tpx_ext(key) for key in tpx_keys} 376 | 377 | # Build new trackpoints with interpolated data 378 | new_trackpoints = [] 379 | for i in range(len(trackpoints)): 380 | new_tpx_ext = {key: interpolated_tpx_ext[key][i] for key in tpx_keys} 381 | new_trackpoint = TCXTrackPoint( 382 | longitude=interpolated_attrs['longitude'][i], 383 | latitude=interpolated_attrs['latitude'][i], 384 | elevation=interpolated_attrs['elevation'][i], 385 | distance=interpolated_attrs['distance'][i], 386 | hr_value=interpolated_attrs['hr_value'][i], 387 | cadence=interpolated_attrs['cadence'][i], 388 | tpx_ext=new_tpx_ext, 389 | time=trackpoints[i].time 390 | ) 391 | new_trackpoints.append(new_trackpoint) 392 | 393 | return new_trackpoints 394 | 395 | def __find_hi_lo_avg(self, tcx: TCXExercise, only_gps: bool) -> TCXExercise: 396 | """ 397 | Finds the highest, lowest, and average values for HR, altitude, cadence, 398 | speeds, etc. Also calculates total ascent/descent. Stores results in 399 | the given TCXExercise or TCXLap (both share the needed fields). 400 | 401 | :param tcx: A TCXExercise (or TCXLap) to modify 402 | :param only_gps: If True, remove any Trackpoints lacking GPS data 403 | :return: The modified tcx (for chaining) 404 | """ 405 | trackpoints = tcx.trackpoints 406 | 407 | if only_gps: 408 | # remove trackpoints that lack longitude 409 | removal_list = [] 410 | for index, tp in enumerate(trackpoints): 411 | if tp.longitude is None: 412 | removal_list.append(index) 413 | for removal in sorted(removal_list, reverse=True): 414 | del trackpoints[removal] 415 | 416 | tcx.trackpoints = trackpoints 417 | 418 | hr = [] 419 | altitude = [] 420 | cadence = [] 421 | 422 | for tp in tcx.trackpoints: 423 | if tp.hr_value is not None: 424 | hr.append(tp.hr_value) 425 | if tp.elevation is not None: 426 | altitude.append(tp.elevation) 427 | if tp.cadence is not None: 428 | cadence.append(tp.cadence) 429 | 430 | # Altitude stats 431 | if altitude: 432 | tcx.altitude_max = max(altitude) 433 | tcx.altitude_min = min(altitude) 434 | tcx.altitude_avg = sum(altitude) / len(altitude) 435 | else: 436 | tcx.altitude_max = None 437 | tcx.altitude_min = None 438 | tcx.altitude_avg = None 439 | 440 | # Ascent / Descent 441 | ascent, descent = 0.0, 0.0 442 | previous_altitude = None 443 | for alt in altitude: 444 | if previous_altitude is None: 445 | previous_altitude = alt 446 | continue 447 | if alt > previous_altitude: 448 | ascent += (alt - previous_altitude) 449 | elif alt < previous_altitude: 450 | descent += (previous_altitude - alt) 451 | previous_altitude = alt 452 | tcx.ascent = ascent 453 | tcx.descent = descent 454 | 455 | # Heart rate stats 456 | if hr: 457 | tcx.hr_max = max(hr) 458 | tcx.hr_min = min(hr) 459 | tcx.hr_avg = sum(hr) / len(hr) 460 | else: 461 | tcx.hr_max = None 462 | tcx.hr_min = None 463 | tcx.hr_avg = None 464 | 465 | # Cadence stats 466 | if cadence: 467 | tcx.cadence_max = max(cadence) 468 | tcx.cadence_avg = sum(cadence) / len(cadence) 469 | else: 470 | tcx.cadence_max = None 471 | tcx.cadence_avg = None 472 | 473 | # tpx_ext data min/max/avg 474 | values = {} 475 | for tp in tcx.trackpoints: 476 | for extension_key, extension_value in tp.tpx_ext.items(): 477 | if extension_value is not None and (isinstance(extension_value, int) or isinstance(extension_value, float)): 478 | if extension_key not in values: 479 | values[extension_key] = [] 480 | values[extension_key].append(extension_value) 481 | 482 | for key, val_list in values.items(): 483 | tcx.tpx_ext_stats[key] = { 484 | "min": min(val_list), 485 | "max": max(val_list), 486 | "avg": sum(val_list) / len(val_list), 487 | } 488 | 489 | # Time-based stats 490 | if len(tcx.trackpoints) > 2: 491 | tcx.start_time = tcx.trackpoints[0].time 492 | tcx.end_time = tcx.trackpoints[-1].time 493 | tcx.duration = abs((tcx.start_time - tcx.end_time).total_seconds()) 494 | # Average speed in km/h 495 | if tcx.duration != 0: 496 | tcx.avg_speed = (tcx.distance / tcx.duration) * 3.6 497 | else: 498 | tcx.avg_speed = 0.0 499 | 500 | # Max speed: analyze consecutive trackpoints 501 | max_speed = 0.0 502 | for i in range(1, len(tcx.trackpoints)): 503 | prev_tp = tcx.trackpoints[i - 1] 504 | curr_tp = tcx.trackpoints[i] 505 | try: 506 | dt = abs((prev_tp.time - curr_tp.time).total_seconds()) 507 | dist = abs(prev_tp.distance - curr_tp.distance) if (prev_tp.distance and curr_tp.distance) else 0.0 508 | speed = (dist / dt) * 3.6 if dt != 0 else 0 509 | if speed > max_speed: 510 | max_speed = speed 511 | except ZeroDivisionError: 512 | print(f"Zero division error at {i}") 513 | pass 514 | except Exception as e: 515 | print(f"Error at {i}: {e}") 516 | pass 517 | tcx.max_speed = max_speed 518 | else: 519 | tcx.start_time = None 520 | tcx.end_time = None 521 | tcx.duration = 0 522 | tcx.avg_speed = 0.0 523 | tcx.max_speed = 0.0 524 | 525 | return tcx 526 | -------------------------------------------------------------------------------- /tcxreader/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alenrajsp/tcxreader/4b0daf5211a7848695918dd84c27ff44891d2e93/tcxreader/tests/__init__.py -------------------------------------------------------------------------------- /tcxreader/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def pytest_configure(config): 5 | r"""Disable verbose output when running tests.""" 6 | logging.basicConfig(level=logging.DEBUG) 7 | -------------------------------------------------------------------------------- /tcxreader/tests/test_reader.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from unittest import TestCase 4 | 5 | from tcxreader.tcxreader import TCXExercise, TCXReader, TCXTrackPoint 6 | 7 | 8 | class TestTCXReader(TestCase): 9 | def setUp(self): 10 | filename_1 = os.path.join(os.path.dirname(__file__), "data", '15.tcx') 11 | filename_2 = os.path.join(os.path.dirname(__file__), "data", 'sup_activity_1.tcx') 12 | filename_3 = os.path.join(os.path.dirname(__file__), "data", 'cross-country-skiing_activity_1.tcx') 13 | self.tcx:TCXExercise = TCXReader().read(filename_1) 14 | self.tcx_sup:TCXExercise = TCXReader().read(filename_2) 15 | self.tcx_cross_country:TCXExercise = TCXReader().read(filename_3) 16 | 17 | def test_distance(self): 18 | self.assertEqual(self.tcx.distance, 116366.98) 19 | 20 | def test_duration(self): 21 | self.assertEqual(self.tcx.duration, 17250.0) 22 | 23 | def test_calories(self): 24 | self.assertEqual(self.tcx.calories, 2010) 25 | 26 | def test_hr_avg(self): 27 | self.assertEqual(int(self.tcx.hr_avg), 140) 28 | 29 | def test_hr_max(self): 30 | self.assertEqual(self.tcx.hr_max, 200) 31 | 32 | def test_hr_min(self): 33 | self.assertEqual(self.tcx.hr_min, 94) 34 | 35 | def altitude_avg(self): 36 | self.assertEqual(self.tcx.altitude_avg, None) 37 | 38 | def test_altitude_min(self): 39 | self.assertAlmostEqual(self.tcx.altitude_min, -5.4, places=1) 40 | 41 | def test_ascent(self): 42 | self.assertAlmostEqual(self.tcx.ascent, 1404.4, places=1) 43 | 44 | def test_descent(self): 45 | self.assertAlmostEqual(self.tcx.descent, 1422.0, places=1) 46 | 47 | def test_author(self): 48 | self.assertEqual(self.tcx_sup.author.name, "Connect Api") 49 | self.assertEqual(self.tcx.author.version_major, 17) 50 | self.assertEqual(self.tcx.author.version_minor, 20) 51 | self.assertEqual(self.tcx.author.build_major, 0) 52 | self.assertEqual(self.tcx.author.build_minor, 0) 53 | 54 | def test_tpx_ext_stats(self): 55 | self.assertAlmostEqual(self.tcx.tpx_ext_stats['Speed']['max'], 18.95800018310547, places=10) 56 | self.assertAlmostEqual(self.tcx.tpx_ext_stats['Speed']['avg'], 7.538691497442126, places=10) 57 | 58 | def test_laps(self): 59 | self.assertEqual(len(self.tcx_cross_country.laps), 2) 60 | self.assertEqual(self.tcx_cross_country.laps[0].lx_ext["AvgRunCadence"], 38) 61 | self.assertEqual(self.tcx_cross_country.laps[0].lx_ext["MaxRunCadence"], 95) 62 | 63 | self.assertEqual(len(self.tcx_cross_country.trackpoints), 64 | len(self.tcx_cross_country.laps[0].trackpoints)+len(self.tcx_cross_country.laps[1].trackpoints)) 65 | 66 | class TestMapMyRideTCX(TestCase): 67 | """Test that reader handles TCX files generated by mapmyride.com / app""" 68 | 69 | def setUp(self): 70 | filename = os.path.join( 71 | os.path.dirname(__file__), "data", "mapmyride_biking.tcx" 72 | ) 73 | self.tcx = TCXReader().read(filename) 74 | 75 | def test_calories(self): 76 | """Handled float fractional calories well enough?""" 77 | self.assertEqual(self.tcx.calories, 458) 78 | 79 | def test_trackpoint_time(self): 80 | """Handled timestamps with timezone listed as '+00:00' ?""" 81 | self.assertEqual( 82 | self.tcx.trackpoints[0].time, 83 | datetime.datetime( 84 | 2022, 11, 12, 15, 58, 31, 473500, tzinfo=datetime.timezone.utc 85 | ), 86 | ) 87 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alenrajsp/tcxreader/4b0daf5211a7848695918dd84c27ff44891d2e93/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_tcxreader.py: -------------------------------------------------------------------------------- 1 | from tcxreader.tests.conftest import pytest_configure 2 | 3 | __all__ = ["pytest_configure"] 4 | 5 | --------------------------------------------------------------------------------