Learn practical skills, build real-world projects, and advance your career

Test demo for functions

try:
  import rosbag
except ModuleNotFoundError:
  !pip3 install rosbag --extra-index-url https://rospypi.github.io/simple/
  import rosbag
Failed to load Python extension for LZ4 support. LZ4 compression will not be available.
if 'google.colab' in str(get_ipython()):
  from google.colab import drive
  from google.colab import files
  drive.mount('/content/drive')
  path = '/content/drive/MyDrive/SA'
  # src = list(files.upload().values())[0]
  # open('functions.py','wb').write(src)
else:
  path = '../data'

#from functions import *
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).

function.py

import rosbag
import random
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import json
import os
import glob
from scipy.spatial import distance
from matplotlib import animation, rc
import PIL
from PIL import Image
from matplotlib.pyplot import cm
from datetime import datetime as dt
from matplotlib.dates import date2num
from scipy import fftpack
from math import pi
import math
import yaml
import warnings
warnings.filterwarnings("ignore")


def read_slam_data(path):
    bag = rosbag.Bag(path)

    conel_x, conel_y = [], []
    coner_x, coner_y = [], []
    car_x, car_y = [], []
    time = []

    for topic, msg, t in bag.read_messages(topics=['/cone_right', '/cone_left', '/pose_stamped']):
        time.append(t)
        if topic == "/cone_right":
            for pose in msg.poses:
                coner_x.append(pose.position.x)
                coner_y.append(pose.position.y)
        elif topic == "/cone_left":
            for pose in msg.poses:
                conel_x.append(pose.position.x)
                conel_y.append(pose.position.y)
        elif topic == "/pose_stamped":
            car_x.append(msg.pose.position.x)
            car_y.append(msg.pose.position.y)

    length = min(len(time), len(car_x), len(car_y))
    pose_dict = {'time': time[:length], 'x': car_x[:length], 'y': car_y[:length]}
    cone_l_dict = {'x': conel_x, 'y': conel_y}
    cone_r_dict = {'x': coner_x, 'y': coner_y}
    car_pose = pd.DataFrame(pose_dict)
    cone_l = pd.DataFrame(cone_l_dict)
    cone_r = pd.DataFrame(cone_r_dict)
    bag.close()

    return car_pose, cone_l.sort_values(by=['x']), cone_r.sort_values(by=['x'])


def scatter_plot_cones_and_car(car_pose, cone_l, cone_r):
    plt.scatter(car_pose['x'], car_pose['y'], color='b')
    plt.scatter(cone_l['x'], cone_l['y'], color='g')
    plt.scatter(cone_r['x'], cone_r['y'], color='g')
    plt.grid(color='lightgray', linestyle='--')
    plt.show()


def smooth_data_fft(arr, span):  # the scaling of "span" is open to suggestions
    w = fftpack.rfft(arr)
    spectrum = w ** 2
    cutoff_idx = spectrum < (spectrum.max() * (1 - np.exp(-span / 2000)))
    w[cutoff_idx] = 0
    return fftpack.irfft(w)


def plot_cones_and_car(car_pose, cone_l, cone_r):
    plt.plot(car_pose['x'], smooth_data_fft(car_pose['y'].to_numpy(), 10), color='b')
    plt.plot(cone_l['x'], smooth_data_fft(cone_l['y'].to_numpy(), 10), color='g')
    plt.plot(cone_r['x'], smooth_data_fft(cone_r['y'].to_numpy(), 10), color='g')
    plt.grid(color='lightgray', linestyle='--')
    plt.show()


def compare_cones_position(cone_l_slam, cone_r_slam, cone_l, cone_r):
    plt.plot(cone_l_slam['x'], cone_l_slam['y'], marker='<', color='y', label='yellow cones slam')
    plt.plot(cone_r_slam['x'], cone_r_slam['y'], marker='<', color='b', label='yellow cones slam')
    plt.plot(cone_l['x'], cone_l['y'], marker='>', color='y', label='yellow cones simulator ')
    plt.plot(cone_r['x'], cone_r['y'], marker='>', color='b', label='yellow cones simulator ')
    plt.grid(color='lightgray', linestyle='--')
    plt.legend()
    plt.show()


def compare_car_position(car_pose_slam, car_pose):
    plt.plot(car_pose_slam['x'], car_pose_slam['y'], marker='<', color='orange', label='car position slam')
    plt.plot(car_pose['x'], car_pose['y'], marker='>', color='gold', label='car position simulator')
    plt.grid(color='lightgray', linestyle='--')
    plt.legend()
    plt.show()


def build_telemetry(path_to_bag):
    b_laps = rosbag.Bag(path_to_bag)
    acc_x, acc_y, acc_z, speed, steer, throttle, brake, gear, pose_x, pose_y, time = [], [], [], [], [], [], [], [], [], [], []

    for topic, msg, t in b_laps.read_messages(topics='/carla/ego_vehicle/vehicle_status'):
        time.append(t)
        acc_x.append(msg.acceleration.linear.x)
        acc_y.append(msg.acceleration.linear.y)
        acc_z.append(msg.acceleration.linear.z)
        speed.append(msg.velocity)
        steer.append(msg.control.steer)
        throttle.append(msg.control.throttle)
        brake.append(msg.control.brake)
        gear.append(msg.control.gear)

    acc = np.linalg.norm(np.vstack((acc_x, acc_y, acc_z)).T, axis=1)
    telemetry_dict = {'time': time, 'speed': speed, 'acceleration': acc, 'steering angle': steer, 'throttle': throttle,
                      'brake': brake, 'gear': gear}
    telemetry = pd.DataFrame(telemetry_dict)

    return telemetry


def find_max_difference(line1: np.ndarray, line2: np.ndarray):
    """
    Finds the point where line1 and line2 differs the most
    :param line1: stream of data, list containing samples for some timesteps
    :param line2: stream of data, list containing samples for some timesteps
    :return: the position (index) where line1 and line2 differ the most and the difference itself
    """
    # if line1 is longer than line2 or vice versa, they will be aligned on their first element
    max_diff, max_diff_idx = 0, 0
    for i in range(min(len(line1), len(line2))):
        diff = np.linalg.norm(line1[i] - line2[i])
        if diff > max_diff:
            max_diff = diff
            max_diff_idx = i
    return max_diff_idx, max_diff


def check_position_accuracy(slam_posits, ground_truth, plot=True):
    """
    This functions creates a plot over the time steps of the difference between the actual position of the car in the
    simulator (ground truth) and the position of the car computed by the SLAM
    """
    # random data only to test the function, comment the following lines for "real" usage
    # ground_truth = np.array([(random.randint(0, 100), random.randint(0, 100)) for _ in range(10)])
    # slam_posits = np.array([(random.randint(0, 100), random.randint(0, 100)) for _ in range(10)])

    # find max difference on x axis and y axis
    idx_x, max_diff_x = find_max_difference(ground_truth[:, 0], slam_posits[:, 0])
    idx_y, max_diff_y = find_max_difference(ground_truth[:, 1], slam_posits[:, 1])

    # plot the differences over time, first on the x axis and then on the y axis
    if plot:
        fig, ax = plt.subplots(1, 2, figsize=(10, 5))
        connection_x = [idx_x, idx_x]
        connection_y = [ground_truth[idx_x][0], slam_posits[idx_x][0]]
        ax[0].plot(ground_truth[:, 0], label="Ground truth")
        ax[0].plot(slam_posits[:, 0], label="SLAM")
        ax[0].plot(connection_x, connection_y, color="red", label="Max difference")
        ax[0].set_title("Position difference on the X axis")
        ax[0].legend()
        connection_x = [idx_y, idx_y]
        connection_y = [ground_truth[idx_y][1], slam_posits[idx_y][1]]
        ax[1].plot(ground_truth[:, 1], label="Ground truth")
        ax[1].plot(slam_posits[:, 1], label="SLAM")
        ax[1].plot(connection_x, connection_y, color="red", label="Max difference")
        ax[1].set_title("Position difference on the Y axis")
        ax[1].legend()
        plt.show()

    # max absolute difference (not along a specific axis)
    _, max_abs_diff = find_max_difference(ground_truth, slam_posits)

    # standard deviation of the difference
    diffs = [np.linalg.norm(ground_truth[i] - slam_posits[i]) for i in range(len(ground_truth))]

    # print results
    print(f"Max difference on x axis: {max_diff_x:.4f}")
    print(f"Max difference on y axis: {max_diff_y:.4f}")
    print(f"Max absolute difference: {max_abs_diff:.4f}")
    print(f"Standard deviation of the difference: {np.std(diffs):.4f}")


def plot_track_layout(path, json_file):
    """
    Plot track layout from json file

    1. plot the track layout from json file
    2. return the position of the yellow/blue cones and the center line
    """
    track_path = os.path.join(path, json_file)

    cone_y_x, cone_y_y = [], []  # yellow cones
    cone_b_x, cone_b_y = [], []  # blue cones
    center_x, center_y = [], []  # center points between yellow and blue cones

    with open(track_path) as f:
        track = json.load(f)

        for id in range(len(track['yellow_cones'])):
            cone_y_x.append(track['yellow_cones'][id]['x'])
            cone_y_y.append(track['yellow_cones'][id]['y'])
            cone_b_x.append(track['blue_cones'][id]['x'])
            cone_b_y.append(track['blue_cones'][id]['y'])
            center_x.append((track['yellow_cones'][id]['x'] + track['blue_cones'][id]['x']) / 2)
            center_y.append((track['yellow_cones'][id]['y'] + track['blue_cones'][id]['y']) / 2)

        cone_y = {'x': cone_y_x, 'y': cone_y_y}
        cone_b = {'x': cone_b_x, 'y': cone_b_y}
        center = {'x': center_x, 'y': center_y}

        plt.plot(cone_y_x, cone_y_y, color='y', label='yellow cones')
        plt.plot(cone_b_x, cone_b_y, color='b', label='blue cones')
        plt.plot(center_x, center_y, color='r', label='center points')
        plt.grid(color='lightgray', linestyle='--')
        plt.legend()

    return cone_y, cone_b, center


def prepare_data(b_laps):
    """
    Read the data from the bag file

    return :
    1. telemetry data
    2. car positions
    """
    acc_x, acc_y, acc_z, speed, steer, throttle, brake, gear, pose_x, pose_y, time = [], [], [], [], [], [], [], [], [], [], []
    car_pose_x, car_pose_y, car_pose_z = [], [], []

    for topic, msg, t in b_laps.read_messages(topics='/carla/ego_vehicle/vehicle_status'):
        time.append(t)
        acc_x.append(msg.acceleration.linear.x)
        acc_y.append(msg.acceleration.linear.y)
        acc_z.append(msg.acceleration.linear.z)
        speed.append(msg.velocity)
        steer.append(msg.control.steer)
        throttle.append(msg.control.throttle)
        brake.append(msg.control.brake)
        gear.append(msg.control.gear)

    for topic, msg, t in b_laps.read_messages(topics='/carla/ego_vehicle/odometry'):
        car_pose_x.append(msg.pose.pose.position.x)
        car_pose_y.append(msg.pose.pose.position.y)
        car_pose_z.append(msg.pose.pose.position.z)

    car_positions = {'x': car_pose_x, 'y': car_pose_y, 'z': car_pose_z}
    acc = np.linalg.norm(np.vstack((acc_x, acc_y, acc_z)).T, axis=1)
    telemetry_dict = {'time': time, 'speed': speed, 'acceleration': acc, 'steering angle': steer, 'throttle': throttle,
                      'brake': brake, 'gear': gear}
    telemetry = pd.DataFrame(telemetry_dict)

    return telemetry, car_positions


def find_lap_indices(car_pose, threshold, skip):
    """
    Find lap indices

    parameters:
    1. car_position dict
    2. use the threshold to define the minimal distance between the start point and the neighbour point specified by threshold.
    3. the number of points to skip for finding the nearest point
    return:
    a dict for each lap the start point and end point
    """
    postion_indices = {}
    lap_indces = []
    lap_dict = {}
    min_distance = distance.euclidean([car_pose['x'][0], car_pose['y'][0]],
                                      [car_pose['x'][threshold], car_pose['y'][threshold]])
    dist = 0
    # print("min_distance: ", min_distance)
    for i in range(skip, len(car_pose['x'])):
        dist = distance.euclidean([car_pose['x'][0], car_pose['y'][0]], [car_pose['x'][i], car_pose['y'][i]])
        if dist < min_distance and car_pose['x'][i] < car_pose['x'][0] < car_pose['x'][(i + 1) % len(car_pose['x'])]:
            lap_indces.append(i)
    lap_dict = {lap_id: (lap_indces[lap_id - 1] + 1 if lap_id > 0 else 0, lap_indces[lap_id]) for lap_id in
                range(len(lap_indces))}

    return lap_dict


def workaroud(center, car_pose):
    """
    workaroud for testing
    parameter:
    1. center line of the tarck layout
    2. dict of the car position
    return:
    1. Adjusted car position
    """
    car_pose_x = [x for x in car_pose['x']]
    car_pose_y = [y for y in car_pose['y']]
    car_pose_x_new = [(x - 215) * 7 / 9 for x in car_pose['x']]
    car_pose_y_new = [(y + 235) * 8 / 9 for y in car_pose['y']]
    car_pose_new = {"x": car_pose_x_new, "y": car_pose_y_new}

    plt.plot(car_pose_x, car_pose_y, color='b', label="original car positions")
    plt.plot(car_pose_x_new, car_pose_y_new, color='g', label="modified car positions")
    plt.plot(center["x"], center["y"], color='r', label="center line")
    plt.grid(color='lightgray', linestyle='--')
    plt.legend()

    return car_pose_new


def project_car_position(laps_indices, center, car_pose, threshold):
    """
    Project the position of the car into center line

    Project the position of the car into center line and plot the projected position of the car for each lap
    parameters:
    1. dict of the laps indices
    2. center line of track layout
    3. dict of car positions
    4. the last threshold index of neighbor to skip when i < 10
    return:
    1. the projected indices for each lap
    2. the position of the car for each lap
    """
    project_laps_indices = {}
    positions_car_x, positions_car_y = [], []
    for lap in range(len(laps_indices)):
        postion_indices = {}
        black_list = set()
        for i in range(len(center["x"])):
            min_distance = float('inf')
            dist = 0
            for j in range(laps_indices[lap][0], laps_indices[lap][1] - (threshold if i < 10 else 0)):
                dist = distance.euclidean([center["x"][i], center["y"][i]], [car_pose["x"][j], car_pose["y"][j]])
                if dist < min_distance and j not in black_list:
                    min_distance = dist
                    postion_indices[i] = j
                    black_list.add(j)
        project_laps_indices[lap] = postion_indices

    for lap in range(len(laps_indices)):
        positions_car_x.append([car_pose["x"][project_laps_indices[lap][id]] for id in range(len(center["x"]))])
        positions_car_y.append([car_pose["y"][project_laps_indices[lap][id]] for id in range(len(center["y"]))])

    car_positions_laps = {"x": positions_car_x, "y": positions_car_y}

    for lap in range(len(laps_indices)):
        plt.scatter(car_positions_laps['x'][lap], car_positions_laps['y'][lap], label="lap " + str(lap))
        plt.grid(color='lightgray', linestyle='--')
        plt.legend()

    return project_laps_indices, car_positions_laps


def delta_plot(telemetry, metric, project_laps_indices, lap_id):
    """
    parameters:
    1. the telemetry data of the laps
    2. a certain metric (e.g. speed)
    3. projected indices for each lap
    4. index of the lap
    """
    assert lap_id > 0

    project_indices = {}
    telemetry_laps = {}
    delta = []

    for lap in range(len(project_laps_indices)):
        project_indices[lap] = [y for (x, y) in project_laps_indices[lap].items()]

    for lap in range(len(project_laps_indices)):
        telemetry_laps[lap] = telemetry.loc[project_indices[lap], 'speed'].reset_index(drop=True)

    for id in range(len(project_laps_indices[lap_id])):
        delta.append(telemetry_laps[lap_id][id] - telemetry_laps[lap_id - 1][id])

    plt.plot(range(len(project_laps_indices[lap_id])), delta)
    plt.ylabel('Position indices of the track')
    plt.ylabel('Delta ' + str(metric))
    plt.grid(color='lightgray', linestyle='--')
    plt.show()


def plot_telemetry(telemetry):
    """
    Plot telemetry data

    parameters:
    1. telemetry to plot
    """
    fig, axs = plt.subplots(len(telemetry.columns) - 1, 1, sharex=True)
    fig.set_size_inches(15, 10)

    for i in range(len(telemetry.columns) - 1):
        axs[i].plot(range(len(telemetry)), telemetry.iloc[:, i + 1])
        axs[i].set_ylabel(telemetry.columns[i + 1])
        axs[i].xaxis.grid(color='gray', linestyle='dashed')
        axs[i].yaxis.grid(color='gray', linestyle='dashed')
        if i == 0:
            axs[i].spines['top'].set_visible(True)
        else:
            axs[i].spines['top'].set_visible(False)
        if i == len(telemetry.columns) - 2:
            axs[i].spines['bottom'].set_visible(True)
        else:
            axs[i].spines['bottom'].set_visible(False)
    plt.subplots_adjust(hspace=0.01)
    plt.show()


def plot_lap_times(telemetry, laps_indices):
    """
     Plot lap times

    parameters:
    1. telemetry data which include timestamps
    2. timestamps indices for each lap
    return:
    1. time used for each lap
    """
    lap_total_times = {}
    for lap in range(len(laps_indices)):
        lap_total_times[lap] = dt.fromtimestamp(
            telemetry.loc[laps_indices[lap][1], 'time'].to_sec()) - dt.fromtimestamp(
            telemetry.loc[laps_indices[lap][0], 'time'].to_sec())

    time_list = [y for (x, y) in lap_total_times.items()]
    min_value = min(time_list)
    min_index = time_list.index(min_value)
    faster = []
    slower = []
    plt.plot(range(len(laps_indices)), [lap_total_times[i].total_seconds() for i in range(len(laps_indices))], 'o-',
             label="lap time")
    for i in range(1, len(laps_indices)):
        if time_list[i] < time_list[i - 1]:
            faster.append(i)
        else:
            slower.append(i)

    plt.plot([i for i in faster], [time_list[i].total_seconds() for i in faster], marker='o', color='g',
             label="faster than the lap before")
    plt.plot([i for i in slower], [time_list[i].total_seconds() for i in slower], marker='o', color='y',
             label="slower than the lap before")

    plt.plot(min_index, min_value.total_seconds(), marker='o', color='purple', label="current fastest lap")

    plt.title('Plot lap times')
    plt.ylabel('Lap time (s)')
    plt.xlabel('Lap number')
    plt.xticks(range(len(laps_indices)))
    plt.grid(color='lightgray', linestyle='--')
    plt.legend()
    plt.show()
    return lap_total_times


def animate_telemetry(telemetry):
    """
    Telemetry data animation

    parameters:
    1. telemetry to animate
    """
    # Create figure for plotting
    fig, axs = plt.subplots(len(telemetry.columns) - 1, 1, sharex=True)
    fig.set_size_inches(10, 8)

    xs = []
    ys = []

    # animation function. This is called sequentially
    def drawframe(n, xs, ys):
        # Add x and y to lists
        xs.append(n)
        ys.append(telemetry.iloc[n, 1:])

        # Limit x and y lists to 20 items
        xs = xs[-20:]
        ys = ys[-20:]

        for i in range(len(telemetry.columns) - 1):
            axs[i].clear()

        for i in range(len(telemetry.columns) - 1):
            axs[i].plot(xs, pd.DataFrame(ys).iloc[:, i])
            axs[i].set_ylabel(telemetry.columns[i + 1])
            axs[i].xaxis.grid(color='gray', linestyle='dashed')
            axs[i].yaxis.grid(color='gray', linestyle='dashed')
            if i == 0:
                axs[i].spines['top'].set_visible(True)
                axs[i].spines['bottom'].set_visible(False)
                axs[i].set_ylim(-1, 16)
            elif i == len(telemetry.columns) - 2:
                axs[i].spines['top'].set_visible(False)
                axs[i].spines['bottom'].set_visible(True)
                axs[i].set_ylim(-0.5, 5.5)
            else:
                axs[i].spines['top'].set_visible(False)
                axs[i].spines['bottom'].set_visible(False)
                axs[i].set_ylim(-0.1, 1.1)
            axs[1].set_ylim(-1, 230)
            axs[2].set_ylim(-0.8, 0.8)
            axs[0].set_title('Telemetry data')
        # Format plot
        plt.subplots_adjust(hspace=0.1)
        # plt.xticks(rotation=45, ha='right')
        plt.subplots_adjust(bottom=0.30)
        if n == len(telemetry) - 1:
            plt.close()

    # Set up plot to call animate() function periodically
    ani = animation.FuncAnimation(fig, drawframe, fargs=(xs, ys), interval=200, frames=len(telemetry))
    rc('animation', html='html5')
    return ani


def compare_telemetry(list_laps):
    """
    Telemetry lines comparison

    parameters:
    1. a list of telemetry data, one lap for each row
    """
    N_laps = len(list_laps)
    color = cm.rainbow(np.linspace(0, 1, len(list_laps)))
    fig, axs = plt.subplots(len(list_laps[0].columns) - 1, 1, sharex=True)
    fig.set_size_inches(15, 10)

    for i in range(len(list_laps[0].columns) - 1):
        for j in range(N_laps):
            axs[i].plot(range(len(list_laps[j])), list_laps[j].iloc[:, i + 1], c=color[j], lw=1, label='lap %s' % j)
            axs[i].set_ylabel(list_laps[j].columns[i + 1])
            axs[i].xaxis.grid(color='gray', linestyle='dashed')
            axs[i].yaxis.grid(color='gray', linestyle='dashed')
            if i == 0:
                axs[i].spines['top'].set_visible(True)
                axs[i].legend(loc="upper right")
            else:
                axs[i].spines['top'].set_visible(False)

            if i == len(list_laps[j].columns) - 2:
                axs[i].spines['bottom'].set_visible(True)
            else:
                axs[i].spines['bottom'].set_visible(False)

    plt.subplots_adjust(hspace=0.01)
    plt.show()


def animate_telemetry_lines_comparison(list_laps):
    """
    Telemetry lines comparison animation

    parameters:
    1. a list of telemetry data, one lap for each row
    """
    # Create figure for plotting
    N_laps = len(list_laps)
    color = cm.rainbow(np.linspace(0, 1, N_laps))
    fig, axs = plt.subplots(len(list_laps[0].columns) - 1, 1, sharex=True)
    fig.set_size_inches(10, 8)

    xs = []
    ys = dict()
    for id in range(N_laps):
        ys[id] = []

    # animation function. This is called sequentially
    def drawframe(n, xs, ys):

        # Add x and y to lists
        xs.append(n)

        for i in range(N_laps):
            ys[i].append(list_laps[i].iloc[n, 1:])

        # Limit x and y lists to 20 items
        xs = xs[-20:]
        for i in range(N_laps):
            ys[i] = ys[i][-20:]

        for i in range(len(list_laps[0].columns) - 1):
            axs[i].clear()

        for i in range(len(list_laps[0].columns) - 1):
            for j in range(N_laps):
                axs[i].plot(xs, pd.DataFrame(ys[j]).iloc[:, i], c=color[j], label='lap %s' % j, lw=1)

                axs[i].set_ylabel(list_laps[0].columns[i + 1])
                axs[i].xaxis.grid(color='gray', linestyle='dashed')
                axs[i].yaxis.grid(color='gray', linestyle='dashed')
                if i == 0:
                    axs[i].spines['top'].set_visible(True)
                    axs[i].spines['bottom'].set_visible(False)
                    axs[i].legend(loc="upper right")
                    axs[i].set_ylim(-1, 16)
                elif i == len(list_laps[0].columns) - 2:
                    axs[i].spines['top'].set_visible(False)
                    axs[i].spines['bottom'].set_visible(True)
                    axs[i].set_ylim(-0.5, 5.5)
                else:
                    axs[i].spines['top'].set_visible(False)
                    axs[i].spines['bottom'].set_visible(False)
                    axs[i].set_ylim(-0.1, 1.1)
                axs[1].set_ylim(-1, 26)
                axs[2].set_ylim(-0.8, 0.8)
                axs[0].set_title('Compare Telemetry data from differnt laps over time')
        # Format plot
        plt.subplots_adjust(hspace=0.1)
        # plt.xticks(rotation=45, ha='right')
        plt.subplots_adjust(bottom=0.30)
        if n == len(list_laps[0]) - 1:
            plt.close()

    # Set up plot to call animate() function periodically
    ani = animation.FuncAnimation(fig, drawframe, fargs=(xs, ys), interval=200, frames=len(list_laps[0]), repeat=False)
    rc('animation', html='html5')
    return ani


def plot_track_layout_depending_on_time_spend(project_laps_indices, telemetry, car_positions_laps, cone_y, cone_b,
                                              lap_id):
    """
    Display the track layout in different colors depending on the time spend for each projected position during the lap

    parameters:
    1. projected indices for each lap
    2. telemetry dataframe
    3. positions of the car for each lap
    4. positions of yellow cones
    5. positions of blue cones
    6. id of the lap
    return:
    1. elemetry dataframe with new columns 'time_spend',	'min_time_spend' and 'max_time_spend'
    """
    project_indices = {}
    time_spend_lap = {}
    min_time_spend_lap = {}
    telemetry_laps = {}
    max_time_spend_lap = {}

    for lap in range(len(project_laps_indices)):
        project_indices[lap] = [y for (x, y) in project_laps_indices[lap].items()]
        time_spend_lap[lap] = [time_spend.to_sec() for time_spend in (
                telemetry.loc[project_indices[lap], "time"].values -
                telemetry.loc[project_indices[lap], "time"].values[0])]

    for loc in range(len(project_indices[0])):
        min_time_spend_lap[loc] = min(
            [time for time in [time_spend_lap[lap][loc] for lap in range(len(project_laps_indices))]])
        max_time_spend_lap[loc] = max(
            [time for time in [time_spend_lap[lap][loc] for lap in range(len(project_laps_indices))]])

    for lap in range(len(project_laps_indices)):
        telemetry_laps[lap] = telemetry.loc[project_indices[lap], :].reset_index(drop=True)
        telemetry_laps[lap].insert(len(telemetry_laps[lap].columns), "time_spend", pd.DataFrame(time_spend_lap[lap]))
        telemetry_laps[lap].insert(len(telemetry_laps[lap].columns), "min_time_spend",
                                   pd.DataFrame(min_time_spend_lap.values()))
        telemetry_laps[lap].insert(len(telemetry_laps[lap].columns), "max_time_spend",
                                   pd.DataFrame(max_time_spend_lap.values()))

    if lap_id > 0:
        # faster than the lap before
        plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
            telemetry_laps[lap_id]['time_spend'] < telemetry_laps[lap_id - 1]['time_spend']].index.tolist()],
                    [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                        telemetry_laps[lap_id]['time_spend'] < telemetry_laps[lap_id - 1][
                            'time_spend']].index.tolist()],
                    color='green', label='faster than the lap before')
        # slower than the lap before
        plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
            telemetry_laps[lap_id]['time_spend'] > telemetry_laps[lap_id - 1]['time_spend']].index.tolist()],
                    [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                        telemetry_laps[lap_id]['time_spend'] > telemetry_laps[lap_id - 1][
                            'time_spend']].index.tolist()],
                    color='gold', label='slower than the lap before')
    else:
        # for lap_id==0 and the slowest
        plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
            telemetry_laps[lap_id]['time_spend'] == telemetry_laps[lap_id]['max_time_spend']].index.tolist()],
                    [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                        telemetry_laps[lap_id]['time_spend'] == telemetry_laps[lap_id][
                            'max_time_spend']].index.tolist()],
                    color='olive', label='current slowest')

    # current fastest
    plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
        telemetry_laps[lap_id]['time_spend'] == telemetry_laps[lap_id]['min_time_spend']].index.tolist()],
                [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                    telemetry_laps[lap_id]['time_spend'] == telemetry_laps[lap_id]['min_time_spend']].index.tolist()],
                color='purple', label='current fastest')

    plt.plot(cone_y['x'], cone_y['y'], color='y', label='yellow cones')
    plt.plot(cone_b['x'], cone_b['y'], color='b', label='blue cones')
    plt.legend()
    plt.grid(color='lightgray', linestyle='--')
    plt.show()

    return telemetry_laps


def plot_track_layout_depending_on_speed(project_laps_indices, telemetry, car_positions_laps, cone_y, cone_b, lap_id):
    """
    Display the track layout in different colors depending on the speed

    parameters:
    1. projected indices for each lap
    2. telemetry dataframe
    3. positions of the car for each lap
    4. positions of yellow cones
    5. positions of blue cones
    6. id of the lap
    return:
    1. elemetry dataframe with new columns 'time_spend',	'min_time_spend' and 'max_time_spend'
    """
    project_indices = {}
    speed_lap = {}
    min_speed_lap = {}
    telemetry_laps = {}
    max_speed_lap = {}

    for lap in range(len(project_laps_indices)):
        project_indices[lap] = [y for (x, y) in project_laps_indices[lap].items()]
        speed_lap[lap] = [speed for speed in telemetry.loc[project_indices[lap], "speed"].values]

    for loc in range(len(project_indices[0])):
        min_speed_lap[loc] = min([speed for speed in [speed_lap[lap][loc] for lap in range(len(project_laps_indices))]])

    for lap in range(len(project_laps_indices)):
        telemetry_laps[lap] = telemetry.loc[project_indices[lap], :].reset_index(drop=True)
        telemetry_laps[lap].insert(len(telemetry_laps[lap].columns), "min_speed", pd.DataFrame(min_speed_lap.values()))

    if lap_id > 0:
        # faster than the lap before
        plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
            telemetry_laps[lap_id]['speed'] < telemetry_laps[lap_id - 1]['speed']].index.tolist()],
                    [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                        telemetry_laps[lap_id]['speed'] < telemetry_laps[lap_id - 1]['speed']].index.tolist()],
                    color='green', label='faster than the lap before')
        # slower than the lap before
        plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
            telemetry_laps[lap_id]['speed'] > telemetry_laps[lap_id - 1]['speed']].index.tolist()],
                    [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                        telemetry_laps[lap_id]['speed'] > telemetry_laps[lap_id - 1]['speed']].index.tolist()],
                    color='red', label='slower than the lap before')

    # current fastest
    plt.scatter([car_positions_laps['x'][lap_id][j] for j in telemetry_laps[lap_id][
        telemetry_laps[lap_id]['speed'] == telemetry_laps[lap_id]['min_speed']].index.tolist()],
                [car_positions_laps['y'][lap_id][j] for j in telemetry_laps[lap_id][
                    telemetry_laps[lap_id]['speed'] == telemetry_laps[lap_id]['min_speed']].index.tolist()],
                color='purple', label='current fastest')

    plt.plot(cone_y['x'], cone_y['y'], color='y', label='yellow cones')
    plt.plot(cone_b['x'], cone_b['y'], color='b', label='blue cones')
    plt.legend()
    plt.grid(color='lightgray', linestyle='--')
    plt.show()

    return telemetry_laps


def display_track_layout_depending_on_gears_used(telemetry, car_pose, cone_y, cone_b):
    """ Display track layout in different colors depending on the gears used

    parameters:
    1. telemetry data to plot
    2. dict of car positions
    3. dict of yellow cones position
    4. dict of blue cones position
    """
    color_list = ['cyan', 'aqua', 'aquamarine', 'lightblue', 'blue', 'darkblue']
    for i in range(6):
        plt.scatter([car_pose['x'][j] for j in telemetry[telemetry['gear'] == i].index.tolist()],
                    [car_pose['y'][j] for j in telemetry[telemetry['gear'] == i].index.tolist()],
                    color=color_list[i], label='gear ' + str(i))
    plt.plot(cone_y['x'], cone_y['y'], color='y', label='yellow cones')
    plt.plot(cone_b['x'], cone_b['y'], color='b', label='blue cones')
    plt.legend()
    plt.grid(color='lightgray', linestyle='--')
    plt.show()


def display_track_layout_depending_on_brake_pressure(telemetry, car_pose, cone_y, cone_b):
    """
    Display track layout in different colors depending on the brake pressure

    parameters:
    1. telemetry data to plot
    2. dict of car positions
    3. dict of yellow cones position
    4. dict of blue cones position
    """
    plt.scatter([car_pose['x'][j] for j in telemetry[telemetry['brake'] == 0].index.tolist()],
                [car_pose['y'][j] for j in telemetry[telemetry['brake'] == 0].index.tolist()],
                color='coral', label='brake pressure == 0')
    plt.scatter([car_pose['x'][j] for j in telemetry[
        (telemetry['brake'] > 0) & (telemetry['brake'] <= telemetry['brake'].mean())].index.tolist()],
                [car_pose['y'][j] for j in telemetry[
                    (telemetry['brake'] > 0) & (telemetry['brake'] <= telemetry['brake'].mean())].index.tolist()],
                color='tomato', label='brake pressure <= mean')
    plt.scatter([car_pose['x'][j] for j in telemetry[
        (telemetry['brake'] > 0) & (telemetry['brake'] <= telemetry['brake'].mean())].index.tolist()],
                [car_pose['y'][j] for j in
                 telemetry[(telemetry['brake'] < 1) & (telemetry['brake'] > telemetry['brake'].mean())].index.tolist()],
                color='red', label='brake pressure >= mean')
    plt.scatter([car_pose['x'][j] for j in telemetry[telemetry['brake'] == 1].index.tolist()],
                [car_pose['y'][j] for j in telemetry[telemetry['brake'] == 1].index.tolist()],
                color='crimson', label='brake pressure == 1')
    plt.plot(cone_y['x'], cone_y['y'], color='y', label='yellow cones')
    plt.plot(cone_b['x'], cone_b['y'], color='b', label='blue cones')
    plt.legend()
    plt.grid(color='lightgray', linestyle='--')
    plt.show()


def animate_percentage(path, percent):
    """
    Display telemetry data as percentage

    parameter:
    1. array of percentage of data to display
    """
    fig, ax = plt.subplots()

    # X and Y coordinates of the center bottom of the needle starting from the top left corner
    #   of the image
    x = 825
    y = 825
    loc = (x, y)

    # ims is a list of lists, each row is a list of gauge to draw in the
    # current frame; here we are just animating one gauge, the image, in
    # each frame
    ims = []
    for i in range(len(percent)):

        rotation = 180 * percent[i]  # 180 degrees because the gauge is half a circle
        rotation = 90 - rotation  # Factor in the needle graphic pointing to 50 (90 degrees)

        dial = Image.open(os.path.join(path, "needle.png"))
        dial = dial.rotate(rotation, resample=PIL.Image.BICUBIC, center=loc)  # Rotate needle

        gauge = Image.open(os.path.join(path, "gauge.png"))
        gauge.paste(dial, mask=dial)  # Paste needle onto gauge

        im = ax.imshow(gauge, animated=True)
        if i == 0:
            ax.imshow(gauge)  # show an initial one first
        ims.append([im])
        plt.close()

    ani = animation.ArtistAnimation(fig, ims, interval=50, blit=True)
    rc('animation', html='html5')
    return ani


def get_cones_positions(coords, ds=0.0002):
    """
    reference:
    https://stackoverflow.com/questions/57065080/draw-perpendicular-line-of-fixed-length-at-a-point-of-another-line
    parameters:
    1. coodinates of circuit
    2. distance between cones and circuit
    return:
    1. positions of yellow cones
    2. positions of blue cones
    """
    cones_yellow = []
    cones_blue = []
    coords = np.array(coords)

    for i in range(1, len(coords)):
        slope = (coords[i][1]-coords[i-1][1])/(coords[i][0]-coords[i-1][0])
        if not math.isinf(slope):
          dy = math.sqrt(ds**2/(slope**2+1))
          dx = -slope*dy
        else:
          dy = 0
          dx = ds
        if (not math.isinf(slope) and np.sign(coords[i][1]-coords[i-1][1]) != np.sign(slope)) or (slope == 0 and coords[i][0]<coords[i-1][0]) or (math.isinf(slope) and coords[i][1]>coords[i-1][1]):
          cones_blue.append({'x' : coords[i-1][0]+dx, 'y': coords[i-1][1]+dy})
          cones_yellow.append({'x' : coords[i-1][0]-dx, 'y': coords[i-1][1]-dy})
        else:
          cones_yellow.append({'x' : coords[i-1][0]+dx, 'y': coords[i-1][1]+dy})
          cones_blue.append({'x' : coords[i-1][0]-dx, 'y': coords[i-1][1]-dy})

    if np.sign(coords[i][1]-coords[i-1][1]) != np.sign(slope):
      cones_blue.append({'x' : coords[i][0]+dx, 'y': coords[i][1]+dy})
      cones_yellow.append({'x' : coords[i][0]-dx, 'y': coords[i][1]-dy})
    else:
      cones_yellow.append({'x' : coords[i][0]+dx, 'y': coords[i][1]+dy})
      cones_blue.append({'x' : coords[i][0]-dx, 'y': coords[i][1]-dy})

    plt.figure(figsize=(15,10))
    plt.plot([item[0] for item in coords], [item[1] for item in coords], '-o', c='coral', label='track layout')
    plt.plot([item["x"] for item in cones_yellow], [item["y"] for item in cones_yellow], '-o', c='gold', label='yellow cones')
    plt.plot([item["x"] for item in cones_blue], [item["y"] for item in cones_blue], '-o', c='turquoise', label='blue cones')
    plt.plot([cones_yellow[0]["x"], cones_blue[0]["x"]], [cones_yellow[0]["y"], cones_blue[0]["y"]], '--s', c='orange', label='start line')    
    plt.grid(color='lightgray',linestyle='--')
    plt.legend()
    plt.show()

    return cones_yellow, cones_blue


def generate_ellipse_shaped_track(x0, y0, rx, ry, path, filename, distance=3):
    """
    parameters:
    1. x0 : x-position of the center
    2. y0 : y-position of the center
    3. rx : radius on the x-axis of track layout
    4. ry : radius on the y-axis of track layout
    5. file path
    6. file name
    7. distance between the cones and track layout
    return:
    1. infomation of the track
    """
    t = np.linspace(0, 2 * pi, 100)
    tx = x0 - rx * np.cos(t)
    ty = y0 - ry * np.sin(t)

    cones = []
    yellow_cones, blue_cones = get_cones_positions(list(zip(tx, ty)), distance)

    cones.append({
        "position": [float(yellow_cones[0]['x']), float(yellow_cones[0]['y']), 0],
        "type": "big orange"
    })

    cones.append({
        "position": [float(blue_cones[0]['x']), float(blue_cones[0]['y']), 0],
        "type": "big orange"
    })

    for i in range(1, len(yellow_cones)):
        cones.append({
            "position": [float(yellow_cones[i]['x']), float(yellow_cones[i]['y']), 0],
            "type": "small yellow"
        })
    for i in range(1, len(blue_cones)):
        cones.append({
            "position": [float(blue_cones[i]['x']), float(blue_cones[i]['y']), 0],
            "type": "small blue"
        })

    data = {
        "name": "ellipse",
        "description": "ellipse-shaped track layout",
        "init_offset": 2,       # move vehicle 2 meters back to align with start line
        "cones": cones
    }

    with open(os.path.join(path, filename), 'w') as outfile:
        yaml.dump(data, outfile, default_flow_style=False)

    return data


def get_circuits_and_cones_postions_from_geojson(path, distance=0.0002):
    """
    parameters:
    1. geojson file path
    2. distance between cones and circuit
    return:
    1. coordinates of circuits
    2. positions of cones
    """
    geojsons = glob.glob(os.path.join(path + '/*.geojson'))
    circuits = {}
    postions_cones = {}
    cones = []

    for idx, geojson in enumerate(geojsons):
        with open(geojson, 'r') as j:
            contents = json.loads(j.read())
            coords = contents['features'][0]['geometry']["coordinates"]
            yellow_cones, blue_cones = get_cones_positions(coords, distance)

            cones.append({
                "position": [float(yellow_cones[0]['x']), float(yellow_cones[0]['y']), 0],
                "type": "big orange"
            })

            cones.append({
                "position": [float(blue_cones[0]['x']), float(blue_cones[0]['y']), 0],
                "type": "big orange"
            })

            for i in range(1, len(yellow_cones)):
                cones.append({
                    "position": [float(yellow_cones[i]['x']), float(yellow_cones[i]['y']), 0],
                    "type": "small yellow"
                })
            for i in range(1, len(blue_cones)):
                cones.append({
                    "position": [float(blue_cones[i]['x']), float(blue_cones[i]['y']), 0],
                    "type": "small blue"
                })

            data = {
                "name": geojson[-15:-8],
                "description": "F1 circuits " + geojson[-15:-8],
                "init_offset": 2,       # move vehicle 2 meters back to align with start line
                "cones": cones
            }

            circuits[idx] = {"x": [item[0] for item in coords], "y": [item[1] for item in coords]}
            postions_cones[idx] = cones

            with open(os.path.join(path, geojson[-15:-8] + '.yaml'), 'w') as outfile:
                yaml.dump(data, outfile, default_flow_style=False)

    return circuits, postions_cones