Merge pull request #404 from SamR1/fix-gpx-wo-elevation
handle gpx file without elevation
This commit is contained in:
commit
b5986b626b
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.0d628d97.js"></script><script defer="defer" src="/static/js/app.c606b7f8.js"></script><link href="/static/css/app.5918c449.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.0d628d97.js"></script><script defer="defer" src="/static/js/app.e8103424.js"></script><link href="/static/css/app.ac01ece3.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
File diff suppressed because one or more lines are too long
2
fittrackee/dist/service-worker.js.map
vendored
2
fittrackee/dist/service-worker.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.c606b7f8.js
vendored
2
fittrackee/dist/static/js/app.c606b7f8.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.e8103424.js
vendored
Normal file
2
fittrackee/dist/static/js/app.e8103424.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.e8103424.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.e8103424.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/workouts.59e60afb.js.map
vendored
Normal file
1
fittrackee/dist/static/js/workouts.59e60afb.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
90
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
90
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
@ -575,6 +575,96 @@ def gpx_file_with_offset() -> str:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_without_elevation() -> str:
|
||||
return (
|
||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||
' <metadata/>'
|
||||
' <trk>'
|
||||
' <name>just a workout</name>'
|
||||
' <trkseg>'
|
||||
' <trkpt lat="44.68095" lon="6.07367">'
|
||||
' <time>2018-03-13T12:44:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68091" lon="6.07367">'
|
||||
' <time>2018-03-13T12:44:50Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.6808" lon="6.07364">'
|
||||
' <time>2018-03-13T12:45:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68075" lon="6.07364">'
|
||||
' <time>2018-03-13T12:45:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68071" lon="6.07364">'
|
||||
' <time>2018-03-13T12:45:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68049" lon="6.07361">'
|
||||
' <time>2018-03-13T12:45:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68019" lon="6.07356">'
|
||||
' <time>2018-03-13T12:45:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68014" lon="6.07355">'
|
||||
' <time>2018-03-13T12:46:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67995" lon="6.07358">'
|
||||
' <time>2018-03-13T12:46:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67977" lon="6.07364">'
|
||||
' <time>2018-03-13T12:46:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67972" lon="6.07367">'
|
||||
' <time>2018-03-13T12:46:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67966" lon="6.07368">'
|
||||
' <time>2018-03-13T12:46:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67961" lon="6.0737">'
|
||||
' <time>2018-03-13T12:46:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67938" lon="6.07377">'
|
||||
' <time>2018-03-13T12:47:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67933" lon="6.07381">'
|
||||
' <time>2018-03-13T12:47:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67922" lon="6.07385">'
|
||||
' <time>2018-03-13T12:47:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67911" lon="6.0739">'
|
||||
' <time>2018-03-13T12:47:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.679" lon="6.07399">'
|
||||
' <time>2018-03-13T12:47:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67896" lon="6.07402">'
|
||||
' <time>2018-03-13T12:47:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67884" lon="6.07408">'
|
||||
' <time>2018-03-13T12:47:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67863" lon="6.07423">'
|
||||
' <time>2018-03-13T12:48:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67858" lon="6.07425">'
|
||||
' <time>2018-03-13T12:48:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67842" lon="6.07434">'
|
||||
' <time>2018-03-13T12:48:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67837" lon="6.07435">'
|
||||
' <time>2018-03-13T12:48:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67822" lon="6.07442">'
|
||||
' <time>2018-03-13T12:48:55Z</time>'
|
||||
' </trkpt>'
|
||||
' </trkseg>'
|
||||
' </trk>'
|
||||
'</gpx>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_wo_track() -> str:
|
||||
return (
|
||||
|
@ -499,6 +499,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||
assert len(data['data']['workouts']) == 1
|
||||
assert_workout_data_with_gpx(data)
|
||||
|
||||
def test_it_adds_a_workout_without_elevation(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
gpx_file_without_elevation: str,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_1.email
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts',
|
||||
data=dict(
|
||||
file=(
|
||||
BytesIO(str.encode(gpx_file_without_elevation)),
|
||||
'example.gpx',
|
||||
),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization=f'Bearer {auth_token}',
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 201
|
||||
workout = data['data']['workouts'][0]
|
||||
assert workout['duration'] == '0:04:10'
|
||||
assert workout['ascent'] is None
|
||||
assert workout['ave_speed'] == 4.57
|
||||
assert workout['descent'] is None
|
||||
assert workout['distance'] == 0.317
|
||||
assert workout['max_alt'] is None
|
||||
assert workout['max_speed'] == 5.1
|
||||
assert workout['min_alt'] is None
|
||||
assert workout['with_gpx'] is True
|
||||
|
||||
def test_it_returns_400_when_quotes_are_not_escaped_in_notes(
|
||||
self,
|
||||
app: Flask,
|
||||
@ -1682,7 +1721,65 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert data['message'] == ''
|
||||
assert data['data']['chart_data'] != ''
|
||||
assert len(data['data']['chart_data']) == gpx_file.count("</trkpt>")
|
||||
assert data['data']['chart_data'][0] == {
|
||||
'distance': 0.0,
|
||||
'duration': 0,
|
||||
'elevation': 998.0,
|
||||
'latitude': 44.68095,
|
||||
'longitude': 6.07367,
|
||||
'speed': 3.21,
|
||||
'time': 'Tue, 13 Mar 2018 12:44:45 GMT',
|
||||
}
|
||||
|
||||
def test_it_gets_chart_data_for_a_workout_created_with_gpx_without_elevation( # noqa
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
gpx_file_without_elevation: str,
|
||||
) -> None:
|
||||
client, auth_token = self.get_test_client_and_auth_token(
|
||||
app, user_1.email
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts',
|
||||
data=dict(
|
||||
file=(
|
||||
BytesIO(str.encode(gpx_file_without_elevation)),
|
||||
'example.gpx',
|
||||
),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization=f'Bearer {auth_token}',
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
workout_short_id = data['data']['workouts'][0]['id']
|
||||
response = client.get(
|
||||
f'/api/workouts/{workout_short_id}/chart_data',
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert data['message'] == ''
|
||||
assert len(
|
||||
data['data']['chart_data']
|
||||
) == gpx_file_without_elevation.count("</trkpt>")
|
||||
# no 'elevation' key in data
|
||||
assert data['data']['chart_data'][0] == {
|
||||
'distance': 0.0,
|
||||
'duration': 0,
|
||||
'latitude': 44.68095,
|
||||
'longitude': 6.07367,
|
||||
'speed': 3.21,
|
||||
'time': 'Tue, 13 Mar 2018 12:44:45 GMT',
|
||||
}
|
||||
|
||||
def test_it_gets_segment_chart_data_for_a_workout_created_with_gpx(
|
||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||
@ -1714,6 +1811,16 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
|
||||
assert 'success' in data['status']
|
||||
assert data['message'] == ''
|
||||
assert data['data']['chart_data'] != ''
|
||||
assert len(data['data']['chart_data']) == gpx_file.count("</trkpt>")
|
||||
assert data['data']['chart_data'][0] == {
|
||||
'distance': 0.0,
|
||||
'duration': 0,
|
||||
'elevation': 998.0,
|
||||
'latitude': 44.68095,
|
||||
'longitude': 6.07367,
|
||||
'speed': 3.21,
|
||||
'time': 'Tue, 13 Mar 2018 12:44:45 GMT',
|
||||
}
|
||||
|
||||
def test_it_returns_403_on_getting_chart_data_if_workout_belongs_to_another_user( # noqa
|
||||
self,
|
||||
|
@ -42,9 +42,14 @@ def get_gpx_data(
|
||||
gpx_data['elevation_max'] = ele.maximum
|
||||
gpx_data['elevation_min'] = ele.minimum
|
||||
|
||||
hill = parsed_gpx.get_uphill_downhill()
|
||||
gpx_data['uphill'] = hill.uphill
|
||||
gpx_data['downhill'] = hill.downhill
|
||||
# gpx file contains elevation data (<ele> element)
|
||||
if ele.maximum is not None:
|
||||
hill = parsed_gpx.get_uphill_downhill()
|
||||
gpx_data['uphill'] = hill.uphill
|
||||
gpx_data['downhill'] = hill.downhill
|
||||
else:
|
||||
gpx_data['uphill'] = None
|
||||
gpx_data['downhill'] = None
|
||||
|
||||
moving_data = parsed_gpx.get_moving_data(
|
||||
stopped_speed_threshold=stopped_speed_threshold
|
||||
@ -237,29 +242,23 @@ def get_chart_data(
|
||||
if segment.get_speed(point_idx) is not None
|
||||
else 0
|
||||
)
|
||||
chart_data.append(
|
||||
{
|
||||
'distance': (
|
||||
round(distance / 1000, 2)
|
||||
if distance is not None
|
||||
else 0
|
||||
),
|
||||
'duration': point.time_difference(first_point),
|
||||
'elevation': (
|
||||
round(point.elevation, 1)
|
||||
if point.elevation is not None
|
||||
else 0
|
||||
),
|
||||
'latitude': point.latitude,
|
||||
'longitude': point.longitude,
|
||||
'speed': speed,
|
||||
# workaround
|
||||
# https://github.com/tkrajina/gpxpy/issues/209
|
||||
'time': point.time.replace(
|
||||
tzinfo=timezone(point.time.utcoffset())
|
||||
),
|
||||
}
|
||||
)
|
||||
data = {
|
||||
'distance': (
|
||||
round(distance / 1000, 2) if distance is not None else 0
|
||||
),
|
||||
'duration': point.time_difference(first_point),
|
||||
'latitude': point.latitude,
|
||||
'longitude': point.longitude,
|
||||
'speed': speed,
|
||||
# workaround
|
||||
# https://github.com/tkrajina/gpxpy/issues/209
|
||||
'time': point.time.replace(
|
||||
tzinfo=timezone(point.time.utcoffset())
|
||||
),
|
||||
}
|
||||
if point.elevation:
|
||||
data['elevation'] = round(point.elevation, 1)
|
||||
chart_data.append(data)
|
||||
previous_point = point
|
||||
previous_distance = distance
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="workout-data"
|
||||
:class="{ 'without-gpx': workout && !workout.with_gpx }"
|
||||
:class="{ 'without-elevation': !hasElevation(workout) }"
|
||||
@click="
|
||||
workout.id
|
||||
? $router.push({
|
||||
@ -92,7 +92,7 @@
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
<div class="data elevation" v-if="workout && workout.with_gpx">
|
||||
<div class="data elevation" v-if="hasElevation(workout)">
|
||||
<img
|
||||
class="mountains"
|
||||
src="/img/workouts/mountains.svg"
|
||||
@ -114,7 +114,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data altitude" v-if="hasElevation(workout)">
|
||||
<div class="data altitude" v-if="hasUphillValue(workout)">
|
||||
<i class="fa fa-location-arrow" aria-hidden="true" />
|
||||
<div class="data-values">
|
||||
+<Distance
|
||||
@ -169,7 +169,16 @@
|
||||
)
|
||||
|
||||
function hasElevation(workout: IWorkout): boolean {
|
||||
return workout && workout.ascent !== null && workout.descent !== null
|
||||
return (
|
||||
workout.with_gpx && workout.min_alt !== null && workout.max_alt !== null
|
||||
)
|
||||
}
|
||||
function hasUphillValue(workout: IWorkout): boolean {
|
||||
return (
|
||||
hasElevation(workout) &&
|
||||
workout.ascent !== null &&
|
||||
workout.descent !== null
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -273,7 +282,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.without-gpx {
|
||||
&.without-elevation {
|
||||
.img,
|
||||
.data {
|
||||
justify-content: center;
|
||||
|
@ -34,7 +34,7 @@
|
||||
{{ $t('workouts.NO_DATA_CLEANING') }}
|
||||
</div>
|
||||
|
||||
<div class="elevation-start">
|
||||
<div class="elevation-start" v-if="hasElevation">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -82,6 +82,9 @@
|
||||
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
|
||||
getDatasets(props.workoutData.chartData, t, props.authUser.imperial_units)
|
||||
)
|
||||
const hasElevation = computed(
|
||||
() => datasets.value && datasets.value.datasets.elevation.data.length > 0
|
||||
)
|
||||
const fromKmUnit = getUnitTo('km')
|
||||
const fromMUnit = getUnitTo('m')
|
||||
const chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
|
||||
@ -141,6 +144,7 @@
|
||||
},
|
||||
yElevation: {
|
||||
beginAtZero: beginElevationAtZero.value,
|
||||
display: hasElevation.value,
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
@ -194,6 +198,7 @@
|
||||
},
|
||||
htmlLegend: {
|
||||
containerID: 'chart-legend',
|
||||
displayElevation: hasElevation.value,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
@ -30,6 +30,12 @@ export const htmlLegendPlugin = {
|
||||
: []
|
||||
|
||||
legendItems.forEach((item: LegendItem) => {
|
||||
if (
|
||||
!chart.config.options?.scales?.yElevation?.display &&
|
||||
item.datasetIndex === 1 // elevation
|
||||
) {
|
||||
return
|
||||
}
|
||||
const li = document.createElement('li')
|
||||
li.onclick = () => {
|
||||
if (item.datasetIndex !== undefined) {
|
||||
|
@ -143,7 +143,7 @@ export type TWorkoutsPayload = TPaginationPayload & {
|
||||
export interface IWorkoutApiChartData {
|
||||
distance: number
|
||||
duration: number
|
||||
elevation: number
|
||||
elevation?: number
|
||||
latitude: number
|
||||
longitude: number
|
||||
speed: number
|
||||
|
@ -43,9 +43,11 @@ export const getDatasets = (
|
||||
datasets.speed.data.push(
|
||||
convertStatsDistance('km', data.speed, useImperialUnits)
|
||||
)
|
||||
datasets.elevation.data.push(
|
||||
convertStatsDistance('m', data.elevation, useImperialUnits)
|
||||
)
|
||||
if (data.elevation !== undefined) {
|
||||
datasets.elevation.data.push(
|
||||
convertStatsDistance('m', data.elevation, useImperialUnits)
|
||||
)
|
||||
}
|
||||
coordinates.push({ latitude: data.latitude, longitude: data.longitude })
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user