Voila gpx viewer¶
View a running version of this notebook. | Download this project.
GPX Viewer¶
This app lets you to display a track from a GPX file recorded with a GPS device.
In [1]:
import datetime
import os
from io import StringIO
from statistics import mean
import gpxpy
import srtm
from bqplot import Axis, Figure, Lines, LinearScale
from bqplot.interacts import IndexSelector
from ipyleaflet import basemaps, FullScreenControl, LayerGroup, Map, MeasureControl, Polyline, Marker, CircleMarker, WidgetControl
from ipywidgets import Button, HTML, HBox, VBox, Checkbox, FileUpload, Label, Output, Layout, Image
from IPython.display import display
In [2]:
tools = ["voila", "ipyleaflet", "ipywidgets", "bqplot"]
logos = []
for tool in tools:
with open(f'./img/{tool}.png', 'rb') as f:
image = f.read()
img = Image(value=image, format='png',layout=Layout(padding='10px'))
logos.append(img)
HBox([Label(value='Powered by:')] + logos, layout=Layout(flex_flow='row', align_items='center'))
In [3]:
# create the output widget to place the results
out = Output()
In [4]:
def parse_data(file):
"""
Parse a GPX file and add elevations
"""
gpx = gpxpy.parse(file)
elevation_data = srtm.get_data()
elevation_data.add_elevations(gpx, smooth=True)
return gpx
In [5]:
def plot_map(gpx):
"""
Plot the GPS trace on a map
"""
points = [p.point for p in gpx.get_points_data(distance_2d=True)]
mean_lat = mean(p.latitude for p in points)
mean_lng = mean(p.longitude for p in points)
# create the map
m = Map(center=(mean_lat, mean_lng), zoom=12, basemap=basemaps.Stamen.Terrain)
# show trace
line = Polyline(locations=[[[p.latitude, p.longitude] for p in points],],
color = "red", fill=False)
m.add_layer(line)
# add markers
waypoints = [
Marker(location=(point.latitude, point.longitude), title=point.name,
popup=HTML(value=point.name), draggable=False)
for point in gpx.waypoints
]
waypoints_layer = LayerGroup(layers=waypoints)
m.add_layer(waypoints_layer)
# add a checkbox to show / hide waypoints
waypoints_checkbox = Checkbox(value=True, description='Show Waypoints')
def update_visible(change):
for p in waypoints:
p.visible = change['new']
waypoints_checkbox.observe(update_visible, 'value')
waypoint_control = WidgetControl(widget=waypoints_checkbox, position='bottomright')
m.add_control(waypoint_control)
# enable full screen mode
m.add_control(FullScreenControl())
# add measure control
measure = MeasureControl(
position='bottomleft',
active_color = 'orange',
primary_length_unit = 'kilometers'
)
m.add_control(measure)
return m
In [6]:
def plot_stats(gpx):
"""
Compute statistics for a given trace
"""
lowest, highest = gpx.get_elevation_extremes()
uphill, downhill = gpx.get_uphill_downhill()
points = gpx.get_points_data(distance_2d=True)
_, distance_from_start, *rest = points[-1]
stat_layout = Layout(margin="10px", padding="10px", border="1px solid black",
flex_flow='column', align_items='center')
stats = [
('Date', gpx.get_time_bounds().start_time.strftime("%Y-%m-%d")),
('Distance', f"{round(distance_from_start / 1000, 2)} km"),
('Duration', str(datetime.timedelta(seconds=gpx.get_duration()))),
('Lowest', f"{int(lowest)} m"),
('Highest', f"{int(highest)} m"),
('Uphill', f"{int(uphill)} m"),
('Downhill', f"{int(downhill)} m"),
]
stats_formatted = [
VBox([
HTML(value=f"<strong>{title}</strong>"),
Label(value=value)
], layout=stat_layout)
for title, value in stats
]
return HBox(stats_formatted, layout=Layout(flex_flow='row', align_items='center'))
In [7]:
def plot_elevation(gpx):
"""
Return an elevation graph for the given gpx trace
"""
points = gpx.get_points_data(distance_2d=True)
px = [p.distance_from_start / 1000 for p in points]
py = [p.point.elevation for p in points]
x_scale, y_scale = LinearScale(), LinearScale()
x_scale.allow_padding = False
x_ax = Axis(label='Distance (km)', scale=x_scale)
y_ax = Axis(label='Elevation (m)', scale=y_scale, orientation='vertical')
lines = Lines(x=px, y=py, scales={'x': x_scale, 'y': y_scale})
elevation = Figure(title='Elevation Chart', axes=[x_ax, y_ax], marks=[lines])
elevation.layout.width = 'auto'
elevation.layout.height = 'auto'
elevation.layout.min_height = '500px'
elevation.interaction = IndexSelector(scale=x_scale)
return elevation
In [8]:
def link_trace_elevation(trace, elevation, gpx, debug):
"""
Link the trace the elevation graph.
Changing the selection on the elevation will update the
marker on the map
"""
points = gpx.get_points_data(distance_2d=True)
_, distance_from_start, *rest = points[-1]
n_points = len(points)
def find_point(distance):
"""
Find a point given the distance
"""
progress = min(1, max(0, distance / distance_from_start))
position = int(progress * (n_points - 1))
return points[position].point
# add a checkbox to auto center
autocenter = Checkbox(value=False, description='Auto Center')
autocenter_control = WidgetControl(widget=autocenter, position='bottomright')
trace.add_control(autocenter_control)
# mark the current position on the map
start = find_point(0)
marker = CircleMarker(visible=False, location=(start.latitude, start.longitude),
radius=10, color="green", fill_color="green")
trace.add_layer(marker)
brushintsel = elevation.interaction
def update_range(change):
"""
Update the position on the map when the elevation
graph selector changes
"""
if brushintsel.selected.shape != (1,):
return
marker.visible = True
selected = brushintsel.selected * 1000 # convert from km to m
point = find_point(selected)
marker.location = (point.latitude, point.longitude)
if autocenter.value:
trace.center = marker.location
brushintsel.observe(update_range, 'selected')
In [9]:
def plot_gpx(gpx_file):
gpx = parse_data(gpx_file)
stats = plot_stats(gpx)
trace = plot_map(gpx)
elevation = plot_elevation(gpx)
debug = Label(value='')
display(stats)
display(trace)
display(elevation)
display(debug)
link_trace_elevation(trace, elevation, gpx, debug)
In [10]:
def show_uploader():
uploader = FileUpload(accept='.gpx', multiple=False)
def handle_upload(change):
# keep only the last file
# TODO: check if this should be fixed in FileUpload widget
# when multiple=False
*_, (_, f) = change['new'].items()
gpx_content = f['content'].decode('utf-8')
out.clear_output()
with StringIO(gpx_content) as gpx_file:
with out:
plot_gpx(gpx_file)
uploader.observe(handle_upload, names='value')
display(uploader)
In [11]:
def show_examples():
example_folder = "./examples"
examples = [f for f in os.listdir(example_folder) if f.endswith('.gpx')]
def create_example(name):
filename = os.path.join(example_folder, name)
@out.capture()
def on_example_clicked(change):
out.clear_output()
with open(filename) as f:
with out:
plot_gpx(f)
button = Button(description=os.path.splitext(name)[0])
button.on_click(on_example_clicked)
return button
buttons = [create_example(example) for example in examples]
line = HBox(buttons, layout=Layout(flex_flow='row', align_items='center'))
display(line)
In [12]:
show_uploader()
In [13]:
show_examples()
In [14]:
# To test without the file uploader
# with open('./trace.gpx') as f:
# plot_gpx(f)
In [15]:
out
This web page was generated from a Jupyter notebook and not all
interactivity will work on this website. Right click to download and run locally for full
Python-backed interactivity.
View a running version of this notebook. | Download this project.