Created
December 3, 2014 00:21
-
-
Save michaelsteffen/38dc106a14115f620f4a to your computer and use it in GitHub Desktop.
Simple script to clean GPX output from a Garmin cycling computer.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Must be run with lxml installed to process larger data sets. | |
# By default looks for ./RawGPX.gpx as the input file and outputs to ./CleanedGPX.gpx. But of course one could change that easily... | |
import gpxpy | |
import gpxpy.gpx | |
import re | |
def get_furthest_point_above_speed(track, start_segment_no, start_point_no, speed, shortest_length=500): | |
""" | |
Starting from a given point on a track, finds the longest subsection of the track | |
for which the average speed is above a given value | |
Parameters | |
---------- | |
track: GPXTrack | |
Track to analyze | |
start_segment_no: integer | |
Index of the segment with the starting point | |
start_point_no : integer | |
Index of the starting point to analyze on the segment | |
speed : integer | |
Speed in m/s, we're looking for a subset of the track with average speed | |
above this value | |
shortest_length : integer | |
Length in meters of the shortest acceptable track -- if the average | |
speed of the initial distance after start_point is not at least this value, | |
return 0 | |
Returns | |
---------- | |
None if no segment is found, otherwise: | |
point: GPXTrackPoint | |
point in the track that marks the furthest end of the matched segment | |
segment_no : integer | |
Index of segment containint point. | |
point_no : integer | |
Index of point. | |
meters: integer | |
Distance between starting point and ending point along the track | |
speed: float | |
Average speed along the identified section of the track in m/s | |
""" | |
# Check that starting point is within bounds -- i.e. if we're beyond the last segment | |
# of the track, or on the last point of a segment | |
if (start_segment_no >= len(track.segments) or | |
start_point_no >= track.segments[start_segment_no].get_points_no()-1): | |
return None | |
meters = 0 | |
seconds = 0 | |
# First time through the loop we start at the point immediately after the starting | |
# point; for subsequent segments, we start at point 0 | |
prev_point = track.segments[start_segment_no].points[start_point_no] | |
start = start_point_no + 1 | |
for segment_no, segment in enumerate(track.segments[start_segment_no:], start_segment_no): | |
for point_no, point in enumerate(segment.points[start:],start): | |
seconds += point.time_difference(prev_point) | |
meters += point.distance_3d(prev_point) or point.distance_2d(prev_point) | |
if (seconds == 0 or meters/seconds < speed): | |
if (seconds == 0 or | |
meters < shortest_length or | |
segment_no == start_segment_no and point_no == start_point_no + 1): | |
return None | |
else: | |
return prev_point, prev_segment_no, prev_point_no, prev_meters, prev_meters/prev_seconds | |
prev_point = point | |
prev_point_no = point_no | |
prev_segment_no = segment_no | |
prev_meters = meters | |
prev_seconds = seconds | |
start = 0 | |
if (meters < shortest_length): | |
return None | |
else: | |
return point, segment_no, point_no, meters, meters/seconds | |
def snip_track(track, segment_no, point_no): | |
""" | |
Shorten a GPXTrack so it ends at the specified point. | |
Parameters | |
---------- | |
track: GPXTrack | |
Track to split | |
segment_no: integer | |
Index of the segment containing the new end point | |
point_no : integer | |
Index of new end point within the segment | |
Returns | |
---------- | |
track: GPXTrack | |
the shortened track | |
""" | |
track.split(segment_no, point_no) | |
track.segments = track.segments[:segment_no+1] | |
return track | |
# ==================== | |
def main(): | |
in_file = open('RawGPX.gpx', 'r') | |
print 'Parsing GPX file . . .' | |
gpx_in = gpxpy.parse(in_file) | |
gpx_out = gpxpy.gpx.GPX() | |
points = 0 | |
print 'Looking for car rides . . .' | |
for track_no, track in enumerate(gpx_in.tracks): | |
time_bounds = track.get_time_bounds() | |
print 'Analyzing track {0} (Start: {1}; End: {2})'.format( | |
track_no, time_bounds.start_time, time_bounds.end_time) | |
# eliminate tracks that don't follow Garmin's naming convention, b/c these are | |
# spurious user-uploaded routes, rather than recorded rides | |
if (re.match(r'\d{4}-\d{2}-\d{2}', track.name) is None): | |
print '--> Removing Non-Ride Track' | |
print ' Track Name: {0}'.format(track.name) | |
continue | |
else: | |
last_point = track.segments[-1].points[-1] | |
for point, segment_no, point_no in track.walk(): | |
# look for segments above ~30 mph = 13.4 m/s | |
fast_info = get_furthest_point_above_speed(track, segment_no, point_no, speed=13.4) | |
if fast_info is not None: | |
(fast_end, fast_segment_no, fast_point_no, fast_meters, fast_speed) = fast_info | |
if (fast_end == last_point): | |
print '--> Removing Car Ride' | |
print ' Car Ride Start: {0}'.format(point.time) | |
print ' Car Ride Length: {0} km'.format(fast_meters/1000) | |
print ' Car Ride Avg Speed: {0} km/h'.format(fast_speed*3.6) | |
track = snip_track(track, segment_no, point_no) | |
break | |
points += 1 | |
gpx_out.tracks.append(track) | |
# output new gpx | |
print 'Saving cleaned GPX file . . .' | |
out_file = open('CleanedGPX.gpx', 'w') | |
out_file.write(gpx_out.to_xml()) | |
print 'Total Points: {0}'.format(points) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment