Merge pull request #404 from SamR1/fix-gpx-wo-elevation

handle gpx file without elevation
This commit is contained in:
Sam 2023-07-19 10:32:53 +02:00 committed by GitHub
commit b5986b626b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 266 additions and 48 deletions

View File

@ -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>

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

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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 (

View File

@ -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,

View File

@ -42,9 +42,14 @@ def get_gpx_data(
gpx_data['elevation_max'] = ele.maximum
gpx_data['elevation_min'] = ele.minimum
# 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,19 +242,11 @@ def get_chart_data(
if segment.get_speed(point_idx) is not None
else 0
)
chart_data.append(
{
data = {
'distance': (
round(distance / 1000, 2)
if distance is not None
else 0
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,
@ -259,7 +256,9 @@ def get_chart_data(
tzinfo=timezone(point.time.utcoffset())
),
}
)
if point.elevation:
data['elevation'] = round(point.elevation, 1)
chart_data.append(data)
previous_point = point
previous_distance = distance

View File

@ -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;

View File

@ -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,
},
},
}))

View File

@ -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) {

View File

@ -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

View File

@ -43,9 +43,11 @@ export const getDatasets = (
datasets.speed.data.push(
convertStatsDistance('km', data.speed, useImperialUnits)
)
if (data.elevation !== undefined) {
datasets.elevation.data.push(
convertStatsDistance('m', data.elevation, useImperialUnits)
)
}
coordinates.push({ latitude: data.latitude, longitude: data.longitude })
})