Merge pull request #109 from SamR1/stopped_fix

Added stopped_speed_threshold to support slow movement
This commit is contained in:
Sam 2021-11-11 17:50:32 +01:00 committed by GitHub
commit d434e82671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 15 deletions

View File

@ -19,6 +19,7 @@
### Pull Requests ### Pull Requests
* [#98/#109](https://github.com/SamR1/FitTrackee/pull/109) - Added stopped_speed_threshold to support slow movement
* [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports * [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports

View File

@ -19,6 +19,7 @@
### Pull Requests ### Pull Requests
* [#98/#109](https://github.com/SamR1/FitTrackee/pull/109) - Added stopped_speed_threshold to support slow movement
* [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports * [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports

View File

@ -51,6 +51,11 @@ Workouts
- Skiing (Cross Country) (**new in 0.5.0**) - Skiing (Cross Country) (**new in 0.5.0**)
- Trail (**new in 0.5.0**) - Trail (**new in 0.5.0**)
- Walking - Walking
- (*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one (0.1 km/h instead of 1 km/h) for the following sports:
- Hiking
- Skiing (Cross Country)
- Trail
- Walking
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts. - Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance) - Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed - A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed

View File

@ -297,6 +297,7 @@
<section id="pull-requests"> <section id="pull-requests">
<h3>Pull Requests<a class="headerlink" href="#pull-requests" title="Permalink to this headline"></a></h3> <h3>Pull Requests<a class="headerlink" href="#pull-requests" title="Permalink to this headline"></a></h3>
<ul class="simple"> <ul class="simple">
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/109">#98/#109</a> - Added stopped_speed_threshold to support slow movement</p></li>
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/93">#84/#93</a> - Add elevation data and new sports</p></li> <li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/93">#84/#93</a> - Add elevation data and new sports</p></li>
</ul> </ul>
</section> </section>

View File

@ -201,6 +201,16 @@
</dd> </dd>
</dl> </dl>
</li> </li>
<li><dl class="simple">
<dt>(<em>new in 0.5.0</em>) Stopped speed threshold used by <a class="reference external" href="https://github.com/tkrajina/gpxpy">gpxpy</a> is not the default one (0.1 km/h instead of 1 km/h) for the following sports:</dt><dd><ul>
<li><p>Hiking</p></li>
<li><p>Skiing (Cross Country)</p></li>
<li><p>Trail</p></li>
<li><p>Walking</p></li>
</ul>
</dd>
</dl>
</li>
<li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.</p></li> <li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.</p></li>
<li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)</p></li> <li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)</p></li>
<li><p>A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed</p></li> <li><p>A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed</p></li>

File diff suppressed because one or more lines are too long

View File

@ -51,6 +51,11 @@ Workouts
- Skiing (Cross Country) (**new in 0.5.0**) - Skiing (Cross Country) (**new in 0.5.0**)
- Trail (**new in 0.5.0**) - Trail (**new in 0.5.0**)
- Walking - Walking
- (*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one (0.1 km/h instead of 1 km/h) for the following sports:
- Hiking
- Skiing (Cross Country)
- Trail
- Walking
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts. - Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user settings). The calendar displays up to 100 workouts.
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance) - Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance)
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed - A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed

View File

@ -0,0 +1,48 @@
"""add stopped speed threshold to sports
Revision ID: 9842464bb885
Revises: cee0830497f8
Create Date: 2021-11-03 21:39:27.310371
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9842464bb885'
down_revision = 'cee0830497f8'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
'sports',
sa.Column('stopped_speed_threshold', sa.Float(), nullable=True),
)
op.execute(
"""
UPDATE sports
SET stopped_speed_threshold = 1
WHERE label in (
'Cycling (Sport)', 'Cycling (Transport)', 'Mountain Biking',
'Mountain Biking (Electric)', 'Rowing', 'Running',
'Skiing (Alpine)'
);
UPDATE sports
SET stopped_speed_threshold = 0.1
WHERE label in (
'Hiking', 'Skiing (Cross Country)', 'Trail', 'Walking'
);
"""
)
op.alter_column('sports', 'stopped_speed_threshold', nullable=False)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('sports', 'stopped_speed_threshold')
# ### end Alembic commands ###

View File

@ -2,9 +2,11 @@ import datetime
from io import BytesIO from io import BytesIO
from typing import Generator from typing import Generator
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from uuid import uuid4
import pytest import pytest
from PIL import Image from PIL import Image
from werkzeug.datastructures import FileStorage
from fittrackee import db from fittrackee import db
from fittrackee.workouts.models import Sport, Workout, WorkoutSegment from fittrackee.workouts.models import Sport, Workout, WorkoutSegment
@ -43,6 +45,7 @@ def sport_1_cycling_inactive() -> Sport:
@pytest.fixture() @pytest.fixture()
def sport_2_running() -> Sport: def sport_2_running() -> Sport:
sport = Sport(label='Running') sport = Sport(label='Running')
sport.stopped_speed_threshold = 0.1
db.session.add(sport) db.session.add(sport)
db.session.commit() db.session.commit()
return sport return sport
@ -575,3 +578,10 @@ def gpx_file_with_segments() -> str:
' </trk>' ' </trk>'
'</gpx>' '</gpx>'
) )
@pytest.fixture()
def gpx_file_storage(gpx_file: str) -> FileStorage:
return FileStorage(
filename=f'{uuid4().hex}.gpx', stream=BytesIO(str.encode(gpx_file))
)

View File

@ -0,0 +1,58 @@
from unittest.mock import call, patch
import pytest
from flask import Flask
from gpxpy.gpx import MovingData
from werkzeug.datastructures import FileStorage
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport
from fittrackee.workouts.utils import process_files
folders = {
'extract_dir': '/tmp/fitTrackee/uploads',
'tmp_dir': '/tmp/fitTrackee/uploads/tmp',
}
moving_data = MovingData(
moving_time=1,
stopped_time=1,
moving_distance=1,
stopped_distance=1,
max_speed=1,
)
class TestStoppedSpeedThreshold:
@pytest.mark.parametrize(
'sport_id, expected_threshold',
[(1, 1.0), (2, 0.1)],
)
def test_it_calls_get_moving_data_with_threshold_depending_on_sport(
self,
app: Flask,
user_1: User,
gpx_file_storage: FileStorage,
sport_1_cycling: Sport,
sport_2_running: Sport,
sport_id: int,
expected_threshold: float,
) -> None:
with patch(
'fittrackee.workouts.utils.get_new_file_path',
return_value='/tmp/fitTrackee/uploads/test.png',
), patch(
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
return_value=moving_data,
) as gpx_track_segment_mock:
process_files(
auth_user_id=user_1.id,
folders=folders,
workout_data={'sport_id': sport_id},
workout_file=gpx_file_storage,
)
assert gpx_track_segment_mock.call_args_list[0] == call(
stopped_speed_threshold=expected_threshold
)
gpx_track_segment_mock.assert_called_with(expected_threshold)

View File

@ -72,6 +72,7 @@ class Sport(BaseModel):
label = db.Column(db.String(50), unique=True, nullable=False) label = db.Column(db.String(50), unique=True, nullable=False)
img = db.Column(db.String(255), unique=True, nullable=True) img = db.Column(db.String(255), unique=True, nullable=True)
is_active = db.Column(db.Boolean, default=True, nullable=False) is_active = db.Column(db.Boolean, default=True, nullable=False)
stopped_speed_threshold = db.Column(db.Float, default=1.0, nullable=False)
workouts = db.relationship( workouts = db.relationship(
'Workout', lazy=True, backref=db.backref('sports', lazy='joined') 'Workout', lazy=True, backref=db.backref('sports', lazy='joined')
) )

View File

@ -289,12 +289,16 @@ def get_map_hash(map_filepath: str) -> str:
return md5.hexdigest() return md5.hexdigest()
def process_one_gpx_file(params: Dict, filename: str) -> Workout: def process_one_gpx_file(
params: Dict, filename: str, stopped_speed_threshold: float
) -> Workout:
""" """
Get all data from a gpx file to create an workout with map image Get all data from a gpx file to create an workout with map image
""" """
try: try:
gpx_data, map_data, weather_data = get_gpx_info(params['file_path']) gpx_data, map_data, weather_data = get_gpx_info(
params['file_path'], stopped_speed_threshold
)
auth_user_id = params['user'].id auth_user_id = params['user'].id
new_filepath = get_new_file_path( new_filepath = get_new_file_path(
auth_user_id=auth_user_id, auth_user_id=auth_user_id,
@ -341,7 +345,9 @@ def process_one_gpx_file(params: Dict, filename: str) -> Workout:
raise WorkoutException('fail', 'Error during workout save.', e) raise WorkoutException('fail', 'Error during workout save.', e)
def process_zip_archive(common_params: Dict, extract_dir: str) -> List: def process_zip_archive(
common_params: Dict, extract_dir: str, stopped_speed_threshold: float
) -> List:
""" """
Get files from a zip archive and create workouts, if number of files Get files from a zip archive and create workouts, if number of files
does not exceed defined limit. does not exceed defined limit.
@ -365,7 +371,9 @@ def process_zip_archive(common_params: Dict, extract_dir: str) -> List:
file_path = os.path.join(extract_dir, gpx_file) file_path = os.path.join(extract_dir, gpx_file)
params = common_params params = common_params
params['file_path'] = file_path params['file_path'] = file_path
new_workout = process_one_gpx_file(params, gpx_file) new_workout = process_one_gpx_file(
params, gpx_file, stopped_speed_threshold
)
new_workouts.append(new_workout) new_workouts.append(new_workout)
return new_workouts return new_workouts
@ -406,9 +414,19 @@ def process_files(
raise WorkoutException('error', 'Error during workout file save.', e) raise WorkoutException('error', 'Error during workout file save.', e)
if extension == ".gpx": if extension == ".gpx":
return [process_one_gpx_file(common_params, filename)] return [
process_one_gpx_file(
common_params,
filename,
sport.stopped_speed_threshold,
)
]
else: else:
return process_zip_archive(common_params, folders['extract_dir']) return process_zip_archive(
common_params,
folders['extract_dir'],
sport.stopped_speed_threshold,
)
def get_upload_dir_size() -> int: def get_upload_dir_size() -> int:

View File

@ -20,6 +20,7 @@ def get_gpx_data(
max_speed: float, max_speed: float,
start: int, start: int,
stopped_time_between_seg: timedelta, stopped_time_between_seg: timedelta,
stopped_speed_threshold: float,
) -> Dict: ) -> Dict:
""" """
Returns data from parsed gpx file Returns data from parsed gpx file
@ -42,7 +43,9 @@ def get_gpx_data(
gpx_data['uphill'] = hill.uphill gpx_data['uphill'] = hill.uphill
gpx_data['downhill'] = hill.downhill gpx_data['downhill'] = hill.downhill
mv = parsed_gpx.get_moving_data() mv = parsed_gpx.get_moving_data(
stopped_speed_threshold=stopped_speed_threshold
)
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time) gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
gpx_data['stop_time'] = ( gpx_data['stop_time'] = (
timedelta(seconds=mv.stopped_time) + stopped_time_between_seg timedelta(seconds=mv.stopped_time) + stopped_time_between_seg
@ -58,6 +61,7 @@ def get_gpx_data(
def get_gpx_info( def get_gpx_info(
gpx_file: str, gpx_file: str,
stopped_speed_threshold: float,
update_map_data: Optional[bool] = True, update_map_data: Optional[bool] = True,
update_weather_data: Optional[bool] = True, update_weather_data: Optional[bool] = True,
) -> Tuple: ) -> Tuple:
@ -104,23 +108,30 @@ def get_gpx_info(
if update_map_data: if update_map_data:
map_data.append([point.longitude, point.latitude]) map_data.append([point.longitude, point.latitude])
segment_max_speed = ( calculated_max_speed = segment.get_moving_data(
segment.get_moving_data().max_speed stopped_speed_threshold=stopped_speed_threshold
if segment.get_moving_data().max_speed ).max_speed
else 0 segment_max_speed = calculated_max_speed if calculated_max_speed else 0
)
if segment_max_speed > max_speed: if segment_max_speed > max_speed:
max_speed = segment_max_speed max_speed = segment_max_speed
segment_data = get_gpx_data( segment_data = get_gpx_data(
segment, segment_max_speed, segment_start, no_stopped_time segment,
segment_max_speed,
segment_start,
no_stopped_time,
stopped_speed_threshold,
) )
segment_data['idx'] = segment_idx segment_data['idx'] = segment_idx
gpx_data['segments'].append(segment_data) gpx_data['segments'].append(segment_data)
full_gpx_data = get_gpx_data( full_gpx_data = get_gpx_data(
gpx, max_speed, start, stopped_time_between_seg gpx,
max_speed,
start,
stopped_time_between_seg,
stopped_speed_threshold,
) )
gpx_data = {**gpx_data, **full_gpx_data} gpx_data = {**gpx_data, **full_gpx_data}