├── .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 TCXExercise 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 - 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 - cadence: int
- distance: float
- elevation: int
- hr_value: int
- latitude: float
- longitude: float
- time: datetime
- cadence: int... tpx_ext: dict
- key1 = value1 (int / float)
- key2 = value2 (int / float)
...
- key1 = value1 (int / float)... 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
- 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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
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 | 2020-12-26T15:50:21.000Z
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 |
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 | 2022-07-16T16:08:25.000Z
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 | 2022-07-16T16:08:37.000Z
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 | 2022-07-16T16:08:39.000Z
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 | 2022-07-16T16:08:40.000Z
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 | 2022-07-16T16:08:47.000Z
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 | 2022-07-16T16:08:49.000Z
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 | 2022-07-16T16:08:50.000Z
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 | 2022-07-16T16:08:56.000Z
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 | 2022-07-16T16:08:57.000Z
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 | 2022-07-16T16:09:10.000Z
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 | 2022-07-16T16:09:14.000Z
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 | 2022-07-16T16:09:50.000Z
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 | 2022-07-16T16:09:55.000Z
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 | 2022-07-16T16:10:00.000Z
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 | 2022-07-16T16:10:29.000Z
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 | 2022-07-16T16:10:36.000Z
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 | 2022-07-16T16:11:15.000Z
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 | 2022-07-16T16:12:03.000Z
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 | 2022-07-16T16:12:04.000Z
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 | 2022-07-16T16:12:05.000Z
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 | 2022-07-16T16:12:13.000Z
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 | 2022-07-16T16:12:17.000Z
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 | 2022-07-16T16:12:22.000Z
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 | 2022-07-16T16:12:26.000Z
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 | 2022-07-16T16:13:04.000Z
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 | 2022-07-16T16:13:24.000Z
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 | 2022-07-16T16:13:27.000Z
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 | 2022-07-16T16:13:28.000Z
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 | 2022-07-16T16:13:29.000Z
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 | 2022-07-16T16:13:30.000Z
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 | 2022-07-16T16:13:31.000Z
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 | 2022-07-16T16:13:35.000Z
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 | 2022-07-16T16:13:36.000Z
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 | 2022-07-16T16:13:39.000Z
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 | 2022-07-16T16:13:43.000Z
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 | 2022-07-16T16:13:45.000Z
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 | 2022-07-16T16:13:46.000Z
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 | 2022-07-16T16:13:47.000Z
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 | 2022-07-16T16:13:55.000Z
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 | 2022-07-16T16:14:04.000Z
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 | 2022-07-16T16:14:17.000Z
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 | 2022-07-16T16:14:59.000Z
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 | 2022-07-16T16:15:06.000Z
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 | 2022-07-16T16:15:09.000Z
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 | 2022-07-16T16:15:25.000Z
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 | 2022-07-16T16:15:48.000Z
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 | 2022-07-16T16:16:41.000Z
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 | 2022-07-16T16:17:22.000Z
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 | 2022-07-16T16:17:28.000Z
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 | 2022-07-16T16:17:31.000Z
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 | 2022-07-16T16:17:33.000Z
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 | 2022-07-16T16:17:39.000Z
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 | 2022-07-16T16:17:45.000Z
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 | 2022-07-16T16:17:51.000Z
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 | 2022-07-16T16:17:56.000Z
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 | 2022-07-16T16:17:58.000Z
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 | 2022-07-16T16:18:00.000Z
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 | 2022-07-16T16:18:18.000Z
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 | 2022-07-16T16:18:27.000Z
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 | 2022-07-16T16:18:30.000Z
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 | 2022-07-16T16:18:59.000Z
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 | 2022-07-16T16:19:01.000Z
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 | 2022-07-16T16:19:14.000Z
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 | 2022-07-16T16:19:15.000Z
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 | 2022-07-16T16:19:44.000Z
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 | 2022-07-16T16:19:58.000Z
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 | 2022-07-16T16:20:02.000Z
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 | 2022-07-16T16:20:04.000Z
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 | 2022-07-16T16:20:14.000Z
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 | 2022-07-16T16:20:19.000Z
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 | 2022-07-16T16:20:23.000Z
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 | 2022-07-16T16:20:48.000Z
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 | 2022-07-16T16:20:51.000Z
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 | 2022-07-16T16:20:54.000Z
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 | 2022-07-16T16:20:56.000Z
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 | 2022-07-16T16:20:58.000Z
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 | 2022-07-16T16:21:14.000Z
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 | 2022-07-16T16:21:23.000Z
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 | 2022-07-16T16:21:25.000Z
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 | 2022-07-16T16:21:34.000Z
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 | 2022-07-16T16:21:45.000Z
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 | 2022-07-16T16:21:58.000Z
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 | 2022-07-16T16:22:28.000Z
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 | 2022-07-16T16:22:32.000Z
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 | 2022-07-16T16:22:34.000Z
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 | 2022-07-16T16:22:57.000Z
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 | 2022-07-16T16:23:04.000Z
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 | 2022-07-16T16:23:06.000Z
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 | 2022-07-16T16:23:11.000Z
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 | 2022-07-16T16:23:16.000Z
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 | 2022-07-16T16:23:20.000Z
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 | 2022-07-16T16:23:32.000Z
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 | 2022-07-16T16:23:57.000Z
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 | 2022-07-16T16:24:01.000Z
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 | 2022-07-16T16:24:19.000Z
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 | 2022-07-16T16:24:25.000Z
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 | 2022-07-16T16:24:27.000Z
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 | 2022-07-16T16:24:32.000Z
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 | 2022-07-16T16:24:46.000Z
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 | 2022-07-16T16:25:05.000Z
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 | 2022-07-16T16:25:23.000Z
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 | 2022-07-16T16:25:34.000Z
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 | 2022-07-16T16:26:07.000Z
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 | 2022-07-16T16:26:20.000Z
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 | 2022-07-16T16:26:21.000Z
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 | 2022-07-16T16:26:26.000Z
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 | 2022-07-16T16:26:36.000Z
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 | 2022-07-16T16:26:47.000Z
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 | 2022-07-16T16:26:48.000Z
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 | 2022-07-16T16:26:49.000Z
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 | 2022-07-16T16:26:57.000Z
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 |
--------------------------------------------------------------------------------