commit bb10bf000930052a47cbff074cbb2e5204ee3897 Author: José Carlos Cuevas Date: Sun Mar 8 18:28:57 2020 +0100 Initial speedometer function created diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/distance.py b/src/distance.py new file mode 100644 index 0000000..e885f31 --- /dev/null +++ b/src/distance.py @@ -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 diff --git a/src/draw/__init__.py b/src/draw/__init__.py new file mode 100644 index 0000000..646d40c --- /dev/null +++ b/src/draw/__init__.py @@ -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) diff --git a/src/draw/elevation.py b/src/draw/elevation.py new file mode 100644 index 0000000..f84d7e1 --- /dev/null +++ b/src/draw/elevation.py @@ -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) diff --git a/src/draw/speedometer.py b/src/draw/speedometer.py new file mode 100644 index 0000000..f81cd2f --- /dev/null +++ b/src/draw/speedometer.py @@ -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() diff --git a/src/parser.py b/src/parser.py new file mode 100644 index 0000000..8ca4743 --- /dev/null +++ b/src/parser.py @@ -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 diff --git a/src/processor.py b/src/processor.py new file mode 100644 index 0000000..cdc8108 --- /dev/null +++ b/src/processor.py @@ -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) diff --git a/src/test.py b/src/test.py new file mode 100644 index 0000000..154d680 --- /dev/null +++ b/src/test.py @@ -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) diff --git a/src/test_draw.py b/src/test_draw.py new file mode 100644 index 0000000..8218a4e --- /dev/null +++ b/src/test_draw.py @@ -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") diff --git a/src/track.py b/src/track.py new file mode 100644 index 0000000..e7e7433 --- /dev/null +++ b/src/track.py @@ -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)