~cypheon/trakka

3d563018ca0e665491eced5ae83bf22d82bb265a — Johann Rudloff 3 years ago
```Calculate climbed height meters during analysis
```
```5 files changed, 71 insertions(+), 8 deletions(-)

M activities/activity.py
M activities/jinja2/details.html
M activities/tests.py
M poetry.lock
M pyproject.toml
```
`M activities/activity.py => activities/activity.py +28 -0`
```@@ 11,6 11,7 @@ import gpxpy
import gpxpy.gpx

import numpy as np
+import scipy.signal
from haversine import haversine, Unit

@@ 207,15 208,32 @@ def guess_type(analysis) -> str:
except:
return 'Exercise'

+def downsample_mean(x, factor):
+    reshaped = x[:(x.shape[0] // factor)*factor].reshape(x.shape[0] // factor, factor)
+    return np.mean(reshaped, axis=1)
+
+def prepare_elevation(dist, elev, smoothness: int):
+    RESOLUTION = 1 # elevation analysis resolution in meters
+
+    # get an array with elements (cumulative distance, altitude)
+    # homogeneously spaced exactly RESOLUTION meters apart
+    dist_lin = np.linspace(0, dist[-1, 1], int(dist[-1, 1]) * RESOLUTION)
+    d_elev_lin = np.interp(dist_lin, dist[:, 1], elev[:, 1])
+
+    return scipy.signal.medfilt(d_elev_lin, 1 + RESOLUTION * 2 * smoothness)
+
# Speed (in m/s) that is considered "moving", lower speeds are considered
# "stopped"
MOVING_THRESHOLD = 0.05

+ELEVATION_SMOOTHNESS = 10 # meters
+
def analyse(track: Track, athlete_data: Mapping[str, float], raw: bool = False):
pos = track.positions
raw_hr = track.hr

dist = np.zeros((pos.shape[0], 2))
+    elev = np.zeros((pos.shape[0], 2))

start_date = pos[0, 0]

@@ 234,6 252,14 @@ def analyse(track: Track, athlete_data: Mapping[str, float], raw: bool = False):
dist[i][0] = (pos[i][0] - start_date).item().total_seconds()
dist[i][1] = length_acc
# print(pos[i])
+        elev[i][0] = dist[i][0]
+        elev[i][1] = pos[i][3]
+    elev[0][1] = elev[1][1]
+
+    prepared = prepare_elevation(dist, elev, ELEVATION_SMOOTHNESS)
+    smooth_elev = downsample_mean(prepared, 2 * ELEVATION_SMOOTHNESS)
+    ele_diff = np.diff(smooth_elev)
+    climb = np.dot(np.greater(ele_diff, 0), ele_diff)

# since time values in the `dist` array are relative to the start, elapsed
# time is just the last time value from the array

@@ 280,6 306,7 @@ def analyse(track: Track, athlete_data: Mapping[str, float], raw: bool = False):

result = {
'distance': dist[-1][1],
+        'climb': climb,
'elapsed_seconds': elapsed_seconds,
'moving_seconds': moving_seconds,
'start_date': pos[0][0].item(),

@@ 292,5 319,6 @@ def analyse(track: Track, athlete_data: Mapping[str, float], raw: bool = False):
if raw:
result['t_dist'] = dist
result['t_hr'] = hr
+        result['t_elev'] = elev

return result

```
`M activities/jinja2/details.html => activities/jinja2/details.html +1 -0`
```@@ 18,6 18,7 @@
{{ stat('Elapsed Time', act.analysis.get('elapsed_seconds') | duration) }}
{{ stat('Moving Time', act.analysis.get('moving_seconds') | duration) }}
{{ stat('Total Distance', act.distance | distance) }}
+    {{ stat('Height Gain', act.analysis.get('climb') | optional_int, 'm') }}
{{ stat('Pace', (act.pace() * 60) | duration, 'min/km') }}
{{ stat('Average Heartrate', act.analysis.get('avg_hr') | optional_int ) }}
{{ stat('Max. Heartrate', act.analysis.get('max_hr') | optional_int) }}

```
`M activities/tests.py => activities/tests.py +5 -0`
```@@ 63,6 63,11 @@ class TestAnalysis(SimpleTestCase):
npt.assert_allclose(a['elapsed_seconds'], 51 * 60 + 23, atol=1)
npt.assert_allclose(a['moving_seconds'], 50 * 60, atol=10)

+    def test_climb(self):
+        a = analyse(self.track, {})
+
+        npt.assert_allclose(a['climb'], 200, atol=10)
+
class TestStravaImport(SimpleTestCase):

def test_import(self):

```
`M poetry.lock => poetry.lock +36 -8`
```@@ 1,7 1,7 @@
[[package]]
category = "dev"
description = "Disable App Nap on macOS >= 10.9"
-marker = "python_version >= \"3.3\" and sys_platform == \"darwin\" or platform_system == \"Darwin\""
+marker = "sys_platform == \"darwin\" or platform_system == \"Darwin\" or python_version >= \"3.3\" and sys_platform == \"darwin\""
name = "appnope"
optional = false
python-versions = "*"

@@ 60,7 60,6 @@ tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0
[[package]]
category = "dev"
description = "Specifications for callback functions passed in to an API"
-marker = "python_version >= \"3.3\""
name = "backcall"
optional = false
python-versions = "*"

@@ 109,7 108,7 @@ version = "4.0.0"
[[package]]
category = "dev"
description = "Cross-platform colored terminal text."
-marker = "python_version >= \"3.3\" and sys_platform == \"win32\""
+marker = "python_version >= \"3.3\" and sys_platform == \"win32\" or sys_platform == \"win32\""
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

@@ 129,7 128,6 @@ six = "*"
[[package]]
category = "dev"
description = "Decorators for Humans"
-marker = "python_version >= \"3.3\""
name = "decorator"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"

@@ 323,7 321,6 @@ test = ["pytest (>=3.6.0)", "pytest-cov", "mock"]
[[package]]
category = "dev"
description = "An autocompletion tool for Python that can be used for text editors."
-marker = "python_version >= \"3.3\""
name = "jedi"
optional = false
python-versions = ">=3.6"

@@ 678,7 675,7 @@ testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
category = "dev"
description = "Pexpect allows easy control of interactive console applications."
-marker = "python_version >= \"3.3\" and sys_platform != \"win32\""
+marker = "python_version >= \"3.3\" and sys_platform != \"win32\" or sys_platform != \"win32\""
name = "pexpect"
optional = false
python-versions = "*"

@@ 690,7 687,6 @@ ptyprocess = ">=0.5"
[[package]]
category = "dev"
description = "Tiny 'shelve'-like database with concurrency support"
-marker = "python_version >= \"3.3\""
name = "pickleshare"
optional = false
python-versions = "*"

@@ 882,6 878,17 @@ security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]

[[package]]
+category = "main"
+description = "SciPy: Scientific Library for Python"
+name = "scipy"
+optional = false
+python-versions = ">=3.7"
+version = "1.6.1"
+
+[package.dependencies]
+numpy = ">=1.16.5"
+
+[[package]]
category = "dev"
description = "Send file to trash natively under Mac OS X, Windows and Linux."
name = "send2trash"

@@ 1007,7 1014,7 @@ version = "3.5.1"
notebook = ">=4.4.1"

-content-hash = "69e2315c6cde9075a37aef19a44539631c00790510e0a249da6e6f808de9eb83"
python-versions = ">=3.9"

@@ 1581,6 1588,27 @@ requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
+scipy = [
+    {file = "scipy-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a15a1f3fc0abff33e792d6049161b7795909b40b97c6cc2934ed54384017ab76"},
+    {file = "scipy-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79570979ccdc3d165456dd62041d9556fb9733b86b4b6d818af7a0afc15f092"},
+    {file = "scipy-1.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a423533c55fec61456dedee7b6ee7dce0bb6bfa395424ea374d25afa262be261"},
+    {file = "scipy-1.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:33d6b7df40d197bdd3049d64e8e680227151673465e5d85723b3b8f6b15a6ced"},
+    {file = "scipy-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:6725e3fbb47da428794f243864f2297462e9ee448297c93ed1dcbc44335feb78"},
+    {file = "scipy-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:5fa9c6530b1661f1370bcd332a1e62ca7881785cc0f80c0d559b636567fab63c"},
+    {file = "scipy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd50daf727f7c195e26f27467c85ce653d41df4358a25b32434a50d8870fc519"},
+    {file = "scipy-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f46dd15335e8a320b0fb4685f58b7471702234cba8bb3442b69a3e1dc329c345"},
+    {file = "scipy-1.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0e5b0ccf63155d90da576edd2768b66fb276446c371b73841e3503be1d63fb5d"},
+    {file = "scipy-1.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2481efbb3740977e3c831edfd0bd9867be26387cacf24eb5e366a6a374d3d00d"},
+    {file = "scipy-1.6.1-cp38-cp38-win32.whl", hash = "sha256:68cb4c424112cd4be886b4d979c5497fba190714085f46b8ae67a5e4416c32b4"},
+    {file = "scipy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:5f331eeed0297232d2e6eea51b54e8278ed8bb10b099f69c44e2558c090d06bf"},
+    {file = "scipy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8a51d33556bf70367452d4d601d1742c0e806cd0194785914daf19775f0e67"},
+    {file = "scipy-1.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:83bf7c16245c15bc58ee76c5418e46ea1811edcc2e2b03041b804e46084ab627"},
+    {file = "scipy-1.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:794e768cc5f779736593046c9714e0f3a5940bc6dcc1dba885ad64cbfb28e9f0"},
+    {file = "scipy-1.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5da5471aed911fe7e52b86bf9ea32fb55ae93e2f0fac66c32e58897cfb02fa07"},
+    {file = "scipy-1.6.1-cp39-cp39-win32.whl", hash = "sha256:8e403a337749ed40af60e537cc4d4c03febddcc56cd26e774c9b1b600a70d3e4"},
+    {file = "scipy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a5193a098ae9f29af283dcf0041f762601faf2e595c0db1da929875b7570353f"},
+    {file = "scipy-1.6.1.tar.gz", hash = "sha256:c4fceb864890b6168e79b0e714c585dbe2fd4222768ee90bc1aa0f8218691b11"},
+]
send2trash = [
{file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"},
{file = "Send2Trash-1.5.0.tar.gz", hash = "sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2"},

```
`M pyproject.toml => pyproject.toml +1 -0`
```@@ 16,6 16,7 @@ djangorestframework = "^3.12.2"
django-oauth-toolkit = "^1.3.3"
gpxpy = "^1.4.2"
matplotlib = "^3.3.3"
+scipy = "^1.6.1"

[tool.poetry.dev-dependencies]
mypy = "^0.812"

```