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() @pytest.fixture()
def gpx_file_wo_track() -> str: def gpx_file_wo_track() -> str:
return ( return (

View File

@ -499,6 +499,45 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
assert len(data['data']['workouts']) == 1 assert len(data['data']['workouts']) == 1
assert_workout_data_with_gpx(data) 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( def test_it_returns_400_when_quotes_are_not_escaped_in_notes(
self, self,
app: Flask, app: Flask,
@ -1682,7 +1721,65 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
assert response.status_code == 200 assert response.status_code == 200
assert 'success' in data['status'] assert 'success' in data['status']
assert data['message'] == '' 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( 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 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 'success' in data['status']
assert data['message'] == '' assert data['message'] == ''
assert data['data']['chart_data'] != '' 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 def test_it_returns_403_on_getting_chart_data_if_workout_belongs_to_another_user( # noqa
self, self,

View File

@ -42,9 +42,14 @@ def get_gpx_data(
gpx_data['elevation_max'] = ele.maximum gpx_data['elevation_max'] = ele.maximum
gpx_data['elevation_min'] = ele.minimum gpx_data['elevation_min'] = ele.minimum
hill = parsed_gpx.get_uphill_downhill() # gpx file contains elevation data (<ele> element)
gpx_data['uphill'] = hill.uphill if ele.maximum is not None:
gpx_data['downhill'] = hill.downhill 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( moving_data = parsed_gpx.get_moving_data(
stopped_speed_threshold=stopped_speed_threshold stopped_speed_threshold=stopped_speed_threshold
@ -237,29 +242,23 @@ def get_chart_data(
if segment.get_speed(point_idx) is not None if segment.get_speed(point_idx) is not None
else 0 else 0
) )
chart_data.append( data = {
{ 'distance': (
'distance': ( round(distance / 1000, 2) if distance is not None else 0
round(distance / 1000, 2) ),
if distance is not None 'duration': point.time_difference(first_point),
else 0 'latitude': point.latitude,
), 'longitude': point.longitude,
'duration': point.time_difference(first_point), 'speed': speed,
'elevation': ( # workaround
round(point.elevation, 1) # https://github.com/tkrajina/gpxpy/issues/209
if point.elevation is not None 'time': point.time.replace(
else 0 tzinfo=timezone(point.time.utcoffset())
), ),
'latitude': point.latitude, }
'longitude': point.longitude, if point.elevation:
'speed': speed, data['elevation'] = round(point.elevation, 1)
# workaround chart_data.append(data)
# https://github.com/tkrajina/gpxpy/issues/209
'time': point.time.replace(
tzinfo=timezone(point.time.utcoffset())
),
}
)
previous_point = point previous_point = point
previous_distance = distance previous_distance = distance

View File

@ -61,7 +61,7 @@
</div> </div>
<div <div
class="workout-data" class="workout-data"
:class="{ 'without-gpx': workout && !workout.with_gpx }" :class="{ 'without-elevation': !hasElevation(workout) }"
@click=" @click="
workout.id workout.id
? $router.push({ ? $router.push({
@ -92,7 +92,7 @@
:useImperialUnits="useImperialUnits" :useImperialUnits="useImperialUnits"
/> />
</div> </div>
<div class="data elevation" v-if="workout && workout.with_gpx"> <div class="data elevation" v-if="hasElevation(workout)">
<img <img
class="mountains" class="mountains"
src="/img/workouts/mountains.svg" src="/img/workouts/mountains.svg"
@ -114,7 +114,7 @@
/> />
</div> </div>
</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" /> <i class="fa fa-location-arrow" aria-hidden="true" />
<div class="data-values"> <div class="data-values">
+<Distance +<Distance
@ -169,7 +169,16 @@
) )
function hasElevation(workout: IWorkout): boolean { 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> </script>
@ -273,7 +282,7 @@
} }
} }
&.without-gpx { &.without-elevation {
.img, .img,
.data { .data {
justify-content: center; justify-content: center;

View File

@ -34,7 +34,7 @@
{{ $t('workouts.NO_DATA_CLEANING') }} {{ $t('workouts.NO_DATA_CLEANING') }}
</div> </div>
<div class="elevation-start"> <div class="elevation-start" v-if="hasElevation">
<label> <label>
<input <input
type="checkbox" type="checkbox"
@ -82,6 +82,9 @@
const datasets: ComputedRef<IWorkoutChartData> = computed(() => const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(props.workoutData.chartData, t, props.authUser.imperial_units) 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 fromKmUnit = getUnitTo('km')
const fromMUnit = getUnitTo('m') const fromMUnit = getUnitTo('m')
const chartData: ComputedRef<ChartData<'line'>> = computed(() => ({ const chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
@ -141,6 +144,7 @@
}, },
yElevation: { yElevation: {
beginAtZero: beginElevationAtZero.value, beginAtZero: beginElevationAtZero.value,
display: hasElevation.value,
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
}, },
@ -194,6 +198,7 @@
}, },
htmlLegend: { htmlLegend: {
containerID: 'chart-legend', containerID: 'chart-legend',
displayElevation: hasElevation.value,
}, },
}, },
})) }))

View File

@ -30,6 +30,12 @@ export const htmlLegendPlugin = {
: [] : []
legendItems.forEach((item: LegendItem) => { legendItems.forEach((item: LegendItem) => {
if (
!chart.config.options?.scales?.yElevation?.display &&
item.datasetIndex === 1 // elevation
) {
return
}
const li = document.createElement('li') const li = document.createElement('li')
li.onclick = () => { li.onclick = () => {
if (item.datasetIndex !== undefined) { if (item.datasetIndex !== undefined) {

View File

@ -143,7 +143,7 @@ export type TWorkoutsPayload = TPaginationPayload & {
export interface IWorkoutApiChartData { export interface IWorkoutApiChartData {
distance: number distance: number
duration: number duration: number
elevation: number elevation?: number
latitude: number latitude: number
longitude: number longitude: number
speed: number speed: number

View File

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