From 8bd0d4c26eea49102329750b3db728a6df568fc1 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 28 May 2018 14:38:32 +0200 Subject: [PATCH] API: get data from gpx to display chart w/ speed and elevation --- mpwo_api/mpwo_api/activities/activities.py | 51 ++++++++++++++++++- mpwo_api/mpwo_api/activities/utils.py | 47 ++++++++++++++++- .../tests/test_activities_api_0_get.py | 48 ++++++++++++++++- .../tests/test_activities_api_1_post.py | 41 +++++++++++++++ 4 files changed, 181 insertions(+), 6 deletions(-) diff --git a/mpwo_api/mpwo_api/activities/activities.py b/mpwo_api/mpwo_api/activities/activities.py index ff31b7c5..31919bb6 100644 --- a/mpwo_api/mpwo_api/activities/activities.py +++ b/mpwo_api/mpwo_api/activities/activities.py @@ -8,8 +8,8 @@ from sqlalchemy import exc from ..users.utils import authenticate, verify_extension from .models import Activity, Sport from .utils import ( - create_activity, create_segment, edit_activity, get_file_path, - get_gpx_info, get_new_file_path + create_activity, create_segment, edit_activity, get_chart_data, + get_file_path, get_gpx_info, get_new_file_path ) activities_blueprint = Blueprint('activities', __name__) @@ -114,6 +114,53 @@ def get_activity_gpx(auth_user_id, activity_id): return jsonify(response_object), code +@activities_blueprint.route( + '/activities//chart_data', methods=['GET'] +) +@authenticate +def get_activity_chart_data(auth_user_id, activity_id): + """Get chart data from an activity gpx file""" + activity = Activity.query.filter_by(id=activity_id).first() + if activity: + if not activity.gpx or activity.gpx == '': + response_object = { + 'status': 'fail', + 'message': f'No gpx file for this activity (id: {activity_id})' + } + return jsonify(response_object), 400 + + try: + chart_content = get_chart_data(activity.gpx) + except Exception as e: + appLog.error(e) + response_object = { + 'status': 'error', + 'message': 'internal error', + 'data': { + 'chart_data': '' + } + } + return jsonify(response_object), 500 + + status = 'success' + message = '' + code = 200 + else: + chart_content = '' + status = 'not found' + message = f'Activity not found (id: {activity_id})' + code = 404 + + response_object = { + 'status': status, + 'message': message, + 'data': { + 'chart_data': chart_content + } + } + return jsonify(response_object), code + + @activities_blueprint.route('/activities', methods=['POST']) @authenticate def post_activity(auth_user_id): diff --git a/mpwo_api/mpwo_api/activities/utils.py b/mpwo_api/mpwo_api/activities/utils.py index 84b91799..c506420c 100644 --- a/mpwo_api/mpwo_api/activities/utils.py +++ b/mpwo_api/mpwo_api/activities/utils.py @@ -122,7 +122,7 @@ def get_gpx_data(parsed_gpx, max_speed, start): return gpx_data -def get_gpx_info(gpx_file): +def open_gpx_file(gpx_file): gpx_file = open(gpx_file, 'r') try: @@ -131,10 +131,17 @@ def get_gpx_info(gpx_file): appLog.error(e) return None - # handle only one track per file if len(gpx.tracks) == 0: return None + return gpx + + +def get_gpx_info(gpx_file): + gpx = open_gpx_file(gpx_file) + if gpx is None: + return None + gpx_data = { 'name': gpx.tracks[0].name, 'segments': [] @@ -173,6 +180,42 @@ def get_gpx_info(gpx_file): return gpx_data +def get_chart_data(gpx_file): + gpx = open_gpx_file(gpx_file) + if gpx is None: + return None + + chart_data = [] + first_point = None + previous_point = None + previous_distance = 0 + + for segment_idx, segment in enumerate(gpx.tracks[0].segments): + for point_idx, point in enumerate(segment.points): + if segment_idx == 0 and point_idx == 0: + first_point = point + distance = (point.distance_3d(previous_point) + if (point.elevation + and previous_point + and previous_point.elevation) + else point.distance_2d(previous_point) + ) + distance = 0 if distance is None else distance + distance += previous_distance + speed = round((segment.get_speed(point_idx) / 1000)*3600, 2) + chart_data.append({ + 'distance': round(distance / 1000, 2), + 'duration': point.time_difference(first_point), + 'elevation': round(point.elevation, 1), + 'speed': speed, + 'time': point.time, + }) + previous_point = point + previous_distance = distance + + return chart_data + + def get_file_path(auth_user_id, activity_file): filename = secure_filename(activity_file.filename) dir_path = os.path.join( diff --git a/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py b/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py index 5d2d7fe9..76f7cb11 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py +++ b/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py @@ -254,8 +254,23 @@ def test_get_an_activity_no_actvity_no_gpx(app, user_1): assert 'Activity not found (id: 11)' in data['message'] assert data['data']['gpx'] == '' + response = client.get( + '/api/activities/11/chart_data', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) -def test_get_an_activity_actvity_no_gpx( + assert response.status_code == 404 + assert 'not found' in data['status'] + assert 'Activity not found (id: 11)' in data['message'] + assert data['data']['chart_data'] == '' + + +def test_get_an_activity_activity_no_gpx( app, user_1, sport_1_cycling, activity_cycling_user_1 ): client = app.test_client() @@ -281,8 +296,22 @@ def test_get_an_activity_actvity_no_gpx( assert 'fail' in data['status'] assert 'No gpx file for this activity (id: 1)' in data['message'] + response = client.get( + '/api/activities/1/chart_data', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) -def test_get_an_activity_actvity_invalid_gpx( + assert response.status_code == 400 + assert 'fail' in data['status'] + assert 'No gpx file for this activity (id: 1)' in data['message'] + + +def test_get_an_activity_activity_invalid_gpx( app, user_1, sport_1_cycling, activity_cycling_user_1 ): activity_cycling_user_1.gpx = "some path" @@ -309,3 +338,18 @@ def test_get_an_activity_actvity_invalid_gpx( assert 'error' in data['status'] assert 'internal error' in data['message'] assert data['data']['gpx'] == '' + + response = client.get( + '/api/activities/1/chart_data', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 500 + assert 'error' in data['status'] + assert 'internal error' in data['message'] + assert data['data']['chart_data'] == '' diff --git a/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py b/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py index b1133ff0..4bcb2ed0 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py +++ b/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py @@ -189,6 +189,47 @@ def test_get_an_activity_with_gpx(app, user_1, sport_1_cycling, gpx_file): assert len(data['data']['gpx']) != '' +def test_get_chart_data_activty_with_gpx( + app, user_1, sport_1_cycling, gpx_file +): + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='12345678' + )), + content_type='application/json' + ) + client.post( + '/api/activities', + data=dict( + file=(BytesIO(str.encode(gpx_file)), 'example.gpx'), + data='{"sport_id": 1}' + ), + headers=dict( + content_type='multipart/form-data', + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + response = client.get( + '/api/activities/1/chart_data', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + assert data['message'] == '' + assert data['data']['chart_data'] != '' + + def test_add_an_activity_with_gpx_without_name( app, user_1, sport_1_cycling, gpx_file_wo_name ):