~cypheon/trakka

3d563018ca0e665491eced5ae83bf22d82bb265a — Johann Rudloff 2 years ago 990f24d
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

# 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"