Skip to content

Commit

Permalink
Add vehicle tracker in GUI
Browse files Browse the repository at this point in the history
  • Loading branch information
toruseo committed May 11, 2024
1 parent 8ea0157 commit f448997
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 15 deletions.
78 changes: 64 additions & 14 deletions uxsim/ResultGUIViewer/ResultGUIViewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import sys
import numpy as np
from matplotlib import colormaps
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu, QSlider, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QPushButton
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu, QSlider, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QPushButton, QInputDialog, QMessageBox, QTableView
from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath
from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer
from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer, QAbstractTableModel

class EdgeItem(QGraphicsItem):
def __init__(self, name, start_node, end_node, density_list, Link):
Expand All @@ -44,7 +44,7 @@ def shape(self):
length = (dx**2 + dy**2)**0.5
offset = length/10
if dx == 0:
offset = QPointF(0, offset if dy > 0 else -offset)
offset = QPointF(-offset*self.curve_direction if dy > 0 else offset*self.curve_direction, 0)
else:
normal = QPointF(-dy, dx)
normal /= (normal.x()**2 + normal.y()**2)**0.5
Expand Down Expand Up @@ -188,9 +188,12 @@ def __init__(self, nodes, edges, vehicle_list):
edge = EdgeItem(name, start_node, end_node, density_list, Node)
self.scene().addItem(edge)
self.edges.append(edge)

self.set_vehice_items()

def set_vehice_items(self):
self.vehicle_items = []
if self.show_vehicles and self.vehicle_list != None:
if self.vehicle_list != None:
for t, edge_name, x in self.vehicle_list:
edge = self.find_edge(edge_name)
if edge:
Expand Down Expand Up @@ -253,15 +256,16 @@ def set_show_vehicles(self, show_vehicles):
self.viewport().update()

class MainWindow(QMainWindow):
def __init__(self, W, nodes, edges, vehicle_list, tmax):
def __init__(self, W, nodes, edges, vehicle_list, tmax, dt):
super().__init__()
self.setWindowTitle("UXsim result viewer")
self.W = W
self.tmax = tmax
self.dt = dt
self.playing = False
self.curve_direction = 1
self.show_names = True
self.show_vehicles = True
self.show_vehicles = False

central_widget = QWidget()
layout = QVBoxLayout()
Expand Down Expand Up @@ -304,6 +308,7 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax):
# menu_file = menu_bar.addMenu("File")
# acrion_save_world = menu_file.addAction("Save World")
# acrion_save_world.triggered.connect(lambda: self.save_world())


menu_settings = menu_bar.addMenu("Settings")
option_curve_direction = menu_settings.addMenu("Link Curve Direction")
Expand All @@ -318,6 +323,14 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax):
show_names_action.setChecked(True)
show_names_action.triggered.connect(self.toggle_show_names)

menu_Vehicle = menu_bar.addMenu("Vehicle Analysis")
# show_vehicles_action = menu_Vehicle.addAction("Show Vehicle")
# show_vehicles_action.setCheckable(True)
# show_vehicles_action.setChecked(False)
# show_vehicles_action.triggered.connect(self.toggle_show_vehicles)
action_show_vehicle = menu_Vehicle.addAction("Highlight Vehicle by ID")
action_show_vehicle.triggered.connect(self.show_vehicle_by_id)

menu_Animation = menu_bar.addMenu("Export Results")
action_csv = menu_Animation.addAction("Export Results to CSV files")
action_csv.triggered.connect(lambda: self.W.analyzer.output_data())
Expand All @@ -328,12 +341,6 @@ def __init__(self, W, nodes, edges, vehicle_list, tmax):
action_network_anim_fancy = menu_Animation.addAction("Export Network Animation (vehicle-level)")
action_network_anim_fancy.triggered.connect(lambda: self.W.analyzer.network_fancy())

# menu_Vehicle = menu_bar.addMenu("Vehicle Analysis")
# show_vehicles_action = menu_Vehicle.addAction("Show Vehicles")
# show_vehicles_action.setCheckable(True)
# show_vehicles_action.setChecked(True)
# show_vehicles_action.triggered.connect(self.toggle_show_vehicles)

self.update_graph()

def save_world(self):
Expand Down Expand Up @@ -377,6 +384,45 @@ def toggle_show_names(self):
def toggle_show_vehicles(self):
self.show_vehicles = not self.show_vehicles
self.graph_widget.set_show_vehicles(self.show_vehicles)

def show_vehicle_by_id(self):
vehicle_id, ok = QInputDialog.getText(self, "Highlight Vehicle", "<b>Enter Vehicle ID</b><br>Note that fast vehicles will be plotted as multiple dots in the animation.")
if ok and vehicle_id:
self.vehicle_id = vehicle_id
if vehicle_id not in self.W.VEHICLES:
QMessageBox.warning(self, "Vehicle ID not found", "The specified Vehicle ID was not found.")
return
veh = self.W.VEHICLES[vehicle_id]
self.graph_widget.vehicle_list = [(int(veh.log_t[i]/self.dt), veh.log_link[i].name, veh.log_x[i]/veh.log_link[i].length) for i in range(len(veh.log_t)) if veh.log_link[i] != -1]
print(veh, self.graph_widget.vehicle_list)

self.graph_widget.set_vehice_items()

self.graph_widget.set_show_vehicles(True)

class PandasModel(QAbstractTableModel):
def __init__(self, data):
super(PandasModel, self).__init__()
self._data = data

def rowCount(self, parent=None):
return self._data.shape[0]

def columnCount(self, parent=None):
return self._data.shape[1]

def data(self, index, role=Qt.DisplayRole):
if index.isValid() and role == Qt.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None

def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
elif orientation == Qt.Vertical:
return str(self._data.index[section])
return None

def launch_World_viewer(W, return_app_window=False):
"""
Expand Down Expand Up @@ -424,10 +470,14 @@ def launch_World_viewer(W, return_app_window=False):
node[2] = (maxy - node[2]) / (maxy - miny) * (xysize - xybuffer*2) + xybuffer

edges = [[l.name, l.start_node.name, l.end_node.name, l.k_mat, l] for l in W.LINKS]
vehicle_list = None
dt = W.LINKS[0].edie_dt

veh = list(W.VEHICLES.values())[0]
vehicle_list = [(int(veh.log_t[i]/dt), veh.log_link[i].name, veh.log_x[i]/veh.log_link[i].length) for i in range(len(veh.log_t)) if veh.log_link[i] != -1]
print(vehicle_list)

app = QApplication(sys.argv)
window = MainWindow(W, nodes, edges, vehicle_list, tmax)
window = MainWindow(W, nodes, edges, None, tmax, dt)
window.show()
if return_app_window:
return app, window
Expand Down
27 changes: 26 additions & 1 deletion uxsim/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def time_space_diagram_traj_links(s, linkslist, figsize=(12,4), plot_signal=True
figsize : tuple of int, optional
The size of the figure to be plotted, default is (12,4).
plot_signal : bool, optional
Plot the signal red light.
Plot the signal red light.
"""
if s.W.vehicle_logging_timestep_interval != 1:
warnings.warn("vehicle_logging_timestep_interval is not 1. The plot is not exactly accurate.", LoggingWarning)
Expand Down Expand Up @@ -1191,6 +1191,31 @@ def log_vehicles_to_pandas(s):
"""
return s.vehicles_to_pandas()

def vehicle_trip_top_pandas(s):
"""
Converts the vehicle trip top to a pandas DataFrame.
Returns
-------
pd.DataFrame
A DataFrame containing the top of the vehicle trip logs, with the columns:
- 'name': the name of the vehicle (platoon).
- 'orig': the origin node of the vehicle's trip.
- 'dest': the destination node of the vehicle's trip.
- 'trip_time': the total trip time of the vehicle.
- 'trip_delay': the total delay of the vehicle.
"""
out = [["name", "orig", "dest", "departure_time", "final_state", "travel_time", "average_speed"]]
for veh in s.W.VEHICLES.values():
veh_dest_name = veh.dest.name if veh.dest != None else None
veh_state = veh.log_state[-1]
veh_ave_speed = np.average([v for v in veh.log_v if v != -1])

out.append([veh.name, veh.orig.name, veh_dest_name, veh.departure_time*s.W.DELTAT, veh_state, veh.travel_time, veh_ave_speed])

s.df_vehicle_trip = pd.DataFrame(out[1:], columns=out[0])
return s.df_vehicle_trip

def basic_to_pandas(s):
"""
Converts the basic stats to a pandas DataFrame.
Expand Down

0 comments on commit f448997

Please sign in to comment.