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
# I feel bad...
@@ 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"
[metadata]
-content-hash = "69e2315c6cde9075a37aef19a44539631c00790510e0a249da6e6f808de9eb83"
+content-hash = "7a9246ed4704c6849ade8cb9ea70d09690847960a115455585ea3dfef5176de6"
python-versions = ">=3.9"
[metadata.files]
@@ 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"