Initial speedometer function created
This commit is contained in:
commit
bb10bf0009
10 changed files with 341 additions and 0 deletions
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
18
src/distance.py
Normal file
18
src/distance.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from math import radians, sin, cos, atan2, sqrt
|
||||
|
||||
EARTH_RADIUS_IN_METERS = 6371007.177356707
|
||||
|
||||
|
||||
def get_meters(lat1, long1, lat2, long2):
|
||||
lat_diff = radians(abs(lat2 - lat1))
|
||||
lng_diff = radians(abs(long2 - long1))
|
||||
|
||||
a = sin(lat_diff/2) * sin(lat_diff/2) + \
|
||||
cos(radians(lat1)) * cos(radians(lat2)) * \
|
||||
sin(lng_diff / 2) * sin(lng_diff / 2)
|
||||
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS_IN_METERS * c
|
34
src/draw/__init__.py
Normal file
34
src/draw/__init__.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
|
||||
import cairo
|
||||
|
||||
|
||||
def initialize(width, height):
|
||||
"""
|
||||
Creates a Cairo Context and initializes it
|
||||
"""
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.set_antialias(cairo.ANTIALIAS_GOOD)
|
||||
return ctx
|
||||
|
||||
|
||||
def set_background(ctx, red, green, blue, alpha=1.0):
|
||||
"""
|
||||
Sets the background to a color
|
||||
"""
|
||||
target = ctx.get_target()
|
||||
width = target.get_width()
|
||||
height = target.get_height()
|
||||
|
||||
ctx.set_source_rgba(red, green, blue, alpha)
|
||||
|
||||
ctx.rectangle(0, 0, width, height)
|
||||
ctx.fill()
|
||||
|
||||
|
||||
def save_to_file(ctx, filename):
|
||||
"""
|
||||
Creates the PNG image out of the given context
|
||||
"""
|
||||
target = ctx.get_target()
|
||||
target.write_to_png(filename)
|
16
src/draw/elevation.py
Normal file
16
src/draw/elevation.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
import cairo
|
||||
|
||||
|
||||
def gauge(ctx, posx, posy, current_value, min_value, max_value, opts):
|
||||
"""
|
||||
Creates a gauge with the desired options
|
||||
"""
|
||||
default_opts = {
|
||||
"width": 50,
|
||||
"height": 100,
|
||||
"color": (0.1, 0.8, 0.1),
|
||||
"text": "Elevation"
|
||||
}
|
||||
final_opts = default_opts.update(opts)
|
71
src/draw/speedometer.py
Normal file
71
src/draw/speedometer.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
"""
|
||||
This module draws a speedometer in the position required
|
||||
with the speed given. It also needs a maximum speed
|
||||
"""
|
||||
|
||||
import math
|
||||
import cairo
|
||||
|
||||
|
||||
def speedometer(ctx, posx, posy, radius, speed, max_speed):
|
||||
# Draw the exterior of the speedometer
|
||||
init_arc = -(5 * math.pi) / 4
|
||||
finish_arc = math.pi / 4
|
||||
ctx.new_sub_path()
|
||||
ctx.arc(posx, posy, radius, init_arc, finish_arc)
|
||||
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.set_line_width(radius * 0.1)
|
||||
ctx.stroke()
|
||||
|
||||
# Draw the interior of the speedometer
|
||||
ctx.new_sub_path()
|
||||
ctx.arc(posx, posy, radius * 0.5, 0, 2 * math.pi)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.set_line_width(radius * 0.1)
|
||||
ctx.stroke()
|
||||
|
||||
# Draw the speed number
|
||||
speed_text = f"{int(round(speed))}"
|
||||
unit_text = "km/h"
|
||||
ctx.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
|
||||
cairo.FONT_WEIGHT_BOLD)
|
||||
|
||||
# The speed itself
|
||||
ctx.set_font_size(radius * 0.4)
|
||||
x_bearing, y_bearing, width, height, x_advance, y_advance = \
|
||||
ctx.text_extents(speed_text)
|
||||
|
||||
x = posx - (width / 2 + x_bearing)
|
||||
y = posy
|
||||
ctx.move_to(x, y)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.show_text(speed_text)
|
||||
ctx.stroke()
|
||||
|
||||
# The unit
|
||||
ctx.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
|
||||
cairo.FONT_WEIGHT_NORMAL)
|
||||
|
||||
ctx.set_font_size(radius * 0.15)
|
||||
x_bearing, y_bearing, width, height, x_advance, y_advance = \
|
||||
ctx.text_extents(unit_text)
|
||||
|
||||
x = posx - (width / 2 + x_bearing)
|
||||
y = y + (radius * 0.2)
|
||||
ctx.move_to(x, y)
|
||||
ctx.show_text(unit_text)
|
||||
ctx.stroke()
|
||||
|
||||
# The curve indicating the speed
|
||||
point_rad = (6 / max_speed) * speed
|
||||
curve_finish_arc = init_arc + ((point_rad * math.pi) / 4)
|
||||
|
||||
ctx.new_sub_path()
|
||||
ctx.arc(posx, posy, radius - (radius * 0.2), init_arc, curve_finish_arc)
|
||||
|
||||
ctx.set_source_rgb(1, 0.1, 0.1)
|
||||
ctx.set_line_width(radius * 0.3)
|
||||
ctx.stroke()
|
78
src/parser.py
Normal file
78
src/parser.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
|
||||
import datetime
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from distance import get_meters
|
||||
from track import Track
|
||||
|
||||
|
||||
def namespace(element):
|
||||
m = re.match(r'\{.*\}', element.tag)
|
||||
if m:
|
||||
name_space = m.group(0)
|
||||
name_space = name_space.replace('{', '')
|
||||
name_space = name_space.replace('}', '')
|
||||
|
||||
return name_space
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def parse_gpx(filename, fps=30.0):
|
||||
root = ET.parse(filename).getroot()
|
||||
print("Default namespace is {}".format(namespace(root)))
|
||||
ns = {'def': namespace(root)}
|
||||
initial_time = datetime.datetime.strptime(root.find('def:metadata', ns).find('def:time', ns).text, "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
|
||||
track_data = root.find('def:trk', ns)
|
||||
lat = None
|
||||
lon = None
|
||||
data = Track()
|
||||
prev_velocity = 0.0
|
||||
prev_elevation = None
|
||||
max_speed = 0.0
|
||||
max_elevation = 0.0
|
||||
|
||||
print("Calculating interpolation at {} fps".format(fps))
|
||||
for segment in track_data.findall('def:trkseg', ns):
|
||||
for point in segment.findall('def:trkpt', ns):
|
||||
new_lat = float(point.attrib['lat'])
|
||||
new_lon = float(point.attrib['lon'])
|
||||
elevation = int(point.find('def:ele', ns).text)
|
||||
|
||||
if not prev_elevation:
|
||||
prev_elevation = elevation
|
||||
|
||||
if lat is None:
|
||||
lat = new_lat
|
||||
|
||||
if lon is None:
|
||||
lon = new_lon
|
||||
|
||||
meters = get_meters(lat, lon, new_lat, new_lon)
|
||||
km_hour = meters * 3.6
|
||||
|
||||
if elevation > max_elevation:
|
||||
max_elevation = elevation
|
||||
|
||||
if km_hour > max_speed:
|
||||
max_speed = km_hour
|
||||
|
||||
# Begin interpolation
|
||||
delta_v = (km_hour - prev_velocity) / fps
|
||||
delta_e = (elevation - prev_elevation) / fps
|
||||
|
||||
for count in range(0, int(fps)):
|
||||
data.add_point(prev_velocity + (count * delta_v), prev_elevation + (count * delta_e))
|
||||
|
||||
# Prepare next iteration
|
||||
lat = new_lat
|
||||
lon = new_lon
|
||||
prev_velocity = km_hour
|
||||
prev_elevation = elevation
|
||||
|
||||
data.set_maximum('speed', max_speed)
|
||||
data.set_maximum('elevation', max_elevation)
|
||||
|
||||
return data
|
59
src/processor.py
Normal file
59
src/processor.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
|
||||
from parser import parse_gpx
|
||||
|
||||
import draw
|
||||
from draw.speedometer import speedometer
|
||||
|
||||
|
||||
FPS = 30.0
|
||||
STILLS_PATH = './stills'
|
||||
|
||||
|
||||
def create_progress_bar(current_frame, last_frame, bar_size=10):
|
||||
percent = current_frame / last_frame
|
||||
fill_bars = math.floor(bar_size * percent)
|
||||
return ("█" * fill_bars) + ("░" * (bar_size - fill_bars))
|
||||
|
||||
|
||||
def create_args_parser():
|
||||
parser = argparse.ArgumentParser(description="Creates frames based off a GPX file for overlays")
|
||||
parser.add_argument('filename', type=str, help="GPX file to parse")
|
||||
parser.add_argument('stills', type=str, help="The folder where to store the still images")
|
||||
parser.add_argument('--fps', dest='fps', default=30, help="Frames per second to generate")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(filename, fps, stills_path):
|
||||
data = parse_gpx(filename, fps)
|
||||
max_speed = data.get_maximum('speed')
|
||||
max_elevation = data.get_maximum('elevation')
|
||||
|
||||
print(f"Replaying at {fps} FPS")
|
||||
print(f"Saving to {stills_path}")
|
||||
|
||||
if not os.path.exists(stills_path):
|
||||
os.makedirs(stills_path)
|
||||
|
||||
frame = 0
|
||||
last_frame = data.length()
|
||||
|
||||
for elem in data.get_datapoints():
|
||||
ctx = draw.initialize(1920, 1080)
|
||||
draw.set_background(ctx, 0, 0, 0, 0.0)
|
||||
progress_bar = create_progress_bar(frame, last_frame, 60)
|
||||
print(f"Current Frame: {frame} {progress_bar} {(frame/last_frame) * 100.0:.2f} %", end="\r")
|
||||
speedometer(ctx, 200, 900, 150, elem[0], max_speed)
|
||||
draw.save_to_file(ctx, os.path.join(stills_path, f"still-{frame:08}.png"))
|
||||
frame += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = create_args_parser()
|
||||
options = parser.parse_args()
|
||||
main(options.filename, options.fps, options.stills)
|
28
src/test.py
Normal file
28
src/test.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import math
|
||||
|
||||
|
||||
from parser import parse_gpx
|
||||
from time import sleep
|
||||
|
||||
|
||||
FPS = 30.0
|
||||
|
||||
|
||||
def create_speed_bar(current_speed, max_speed, bar_size=10):
|
||||
percent = current_speed / max_speed
|
||||
fill_bars = math.floor(bar_size * percent)
|
||||
return ("█" * fill_bars) + ("░" * (bar_size - fill_bars))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = parse_gpx('../data/2019_08_08_1.gpx', FPS)
|
||||
max_speed = data.get_maximum('speed')
|
||||
|
||||
print(f"Replaying at {FPS} FPS")
|
||||
|
||||
for elem in data.get_datapoints():
|
||||
speed_bar = create_speed_bar(elem[0], max_speed, 50)
|
||||
print("Speed: {} {:.0f} km/h - Elevation: {:.0f} meters ".format(speed_bar, elem[0], elem[1]), end="\r")
|
||||
sleep(1/FPS)
|
11
src/test_draw.py
Normal file
11
src/test_draw.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
import draw
|
||||
from draw.speedometer import speedometer
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ctx = draw.initialize(1920, 1080)
|
||||
draw.set_background(ctx, 0, 0, 0, 0.0)
|
||||
speedometer(ctx, 200, 900, 150, 0, 100)
|
||||
draw.save_to_file(ctx, "test.png")
|
26
src/track.py
Normal file
26
src/track.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
|
||||
class Track:
|
||||
"""
|
||||
Holds all the data generated from processing the GPX file
|
||||
"""
|
||||
def __init__(self):
|
||||
self.datapoints = []
|
||||
self.maximums = {}
|
||||
|
||||
def add_point(self, speed, elevation):
|
||||
self.datapoints.append((speed, elevation))
|
||||
|
||||
def set_datapoins(self, datapoints):
|
||||
self.datapoints = datapoints
|
||||
|
||||
def get_datapoints(self):
|
||||
return self.datapoints
|
||||
|
||||
def length(self):
|
||||
return len(self.datapoints)
|
||||
|
||||
def set_maximum(self, key, value):
|
||||
self.maximums[key] = value
|
||||
|
||||
def get_maximum(self, key):
|
||||
return self.maximums.get(key, 0)
|
Loading…
Reference in a new issue