#
# Copyright (c) 2020-2025 Julian Heinovski <heinovski@ccs-labs.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
import logging
import os
import sys
LOG = logging.getLogger(__name__)
TRACI_SUPPORTED_VERSION = 20
SUMO_SUPPORTED_VERSIONS = [
"1.6.0",
]
[docs]def check_and_prepare_gui():
"""
Check and prepare GUI environment.
"""
if "SUMO_HOME" not in os.environ:
sys.exit(f"ERROR [{__name__}]: Environment variable 'SUMO_HOME' is not declared! Have you installed SUMO?")
tools = os.path.join(os.environ["SUMO_HOME"], "tools")
sys.path.append(tools)
# check TraCI API version
import traci # noqa C415
api_version = traci.constants.TRACI_VERSION
assert api_version
if api_version != TRACI_SUPPORTED_VERSION:
sys.exit(f"ERROR [{__name__}]: You are using an unsupported TraCI version ({api_version})! Make sure to install a SUMO version with TraCI API version {TRACI_SUPPORTED_VERSION}.")
[docs]def start_gui(config: str, step_length: float, play: bool = True):
"""
Start the GUI.
Parameters
----------
config : str
The name of the configuration file
step_length : float
The length of one simulation step in s
play : bool, optional
Whether to start the simulation automatically
"""
binary = os.path.join(os.environ["SUMO_HOME"], "bin/sumo-gui")
command = [
binary,
"-Q",
"-c",
config,
"--step-length",
str(step_length),
"--collision.mingap-factor",
"0",
"--collision.action",
"none",
"--start",
str(play),
]
import traci # noqa C415
traci.start(command)
# check TraCI API and SUMO version
# TODO perform this check before the actual GUI has been opened
try:
api_version, sumo_version = traci.getVersion()
except AttributeError:
pass
assert api_version and sumo_version
# check TraCI API version again for safety
assert api_version == traci.constants.TRACI_VERSION
# remove "SUMO" prefix
sumo_version = sumo_version.split(" ")[1]
assert sumo_version
# NOTE: we allow other SUMO versions with the correct TraCI API_version version (e.g., development versions).
# However, these versions are untested and thus might lead lead to unexpected behavior.
if sumo_version not in SUMO_SUPPORTED_VERSIONS:
LOG.warning(f"You are using an untested SUMO version ({sumo_version})! We recommend one of {SUMO_SUPPORTED_VERSIONS}.")
[docs]def set_gui_window(road_length: int):
"""
Set the window of the GUI according to the road length.
Parameters
----------
road_length : int
The length of the road in m
"""
import traci # noqa C415
# set window bounday
# TODO make y-lim dependet on road length
traci.gui.setBoundary(
traci.gui.DEFAULT_VIEW,
xmin=0,
ymin=-25000,
xmax=road_length,
ymax=25000,
)
# set zoom level based on raod length
zoom_factor = (1e8 / road_length) * 0.97
traci.gui.setZoom(traci.gui.DEFAULT_VIEW, zoom_factor)
[docs]def add_gui_vehicle(
vid: int,
position: float,
lane: int,
speed: float,
color: tuple = (0, 255, 0),
track: bool = False,
):
"""
Add a vehicle to the GUI.
Parameters
----------
vid : int
The vehicle's id
position : float
The vehicle's current position
lane : int
The vehicle's current lane
speed : float
The vehicle's current speed
color : tuple
The vehicle's current color
track : bool
Whether to track this vehicle within the GUI
"""
LOG.trace(f"Adding vehicle {vid} at {position},{lane} with {speed},{color}")
import traci # noqa C415
if vid not in traci.vehicle.getIDList():
traci.vehicle.add(
vehID=str(vid),
routeID="route",
typeID="vehicle",
departLane=lane,
departPos=position,
departSpeed=speed,
)
traci.vehicle.setColor(str(vid), color)
traci.vehicle.setSpeedMode(str(vid), 0)
traci.vehicle.setLaneChangeMode(str(vid), 0)
# track vehicle
if track:
traci.gui.trackVehicle(traci.gui.DEFAULT_VIEW, str(vid))
traci.gui.setZoom(traci.gui.DEFAULT_VIEW, 700000)
[docs]def move_gui_vehicle(vid: int, position: float, lane: int, speed: float):
"""
Move a vehicle in the GUI.
Parameters
----------
vid : int
The id of the vehicle to change
position : float
The vehicle's new position
lane : int
The vehicle's new lane
speed : float
The vehicle's new speed
"""
import traci # noqa C415
LOG.trace(f"Moving vehicle {vid} to {position},{lane} with {speed}")
traci.vehicle.setSpeed(vehID=str(vid), speed=speed)
traci.vehicle.moveTo(vehID=str(vid), laneID=f"edge_0_0_{lane}", pos=position)
[docs]def gui_step(target_step: int, screenshot_filename: str = None):
"""
Increases the simulation step in the GUI.
Parameters
----------
target_step : int
The target simulation step
screenshot_filename : str, optional
The name of the screenshot file
"""
import traci # noqa C415
if screenshot_filename:
file_name, file_extension = os.path.splitext(screenshot_filename)
traci.gui.screenshot(
traci.gui.DEFAULT_VIEW,
f"{file_name}_{traci.simulation.getTime():06.1f}.{file_extension if file_extension else 'png'}",
1920,
1080,
)
traci.simulationStep(target_step)
assert traci.simulation.getTime() == float(target_step)
[docs]def change_gui_vehicle_color(vid: int, color: tuple):
"""
Change the color of a vehicle in the GUI.
Parameters
----------
vid : int
The id of the vehicle to change
color : tuple
The color (R, G, B) to use for the vehicle
"""
import traci # noqa C415
LOG.trace(f"Changing color of vehicle {vid} to {color}")
traci.vehicle.setColor(str(vid), color)
[docs]def remove_gui_vehicle(vid: int):
"""
Remove a vehicle from the GUI.
Parameters
----------
vid : int
The id of the vehicle to remove
"""
import traci # noqa C415
LOG.trace(f"Removing vehicle {vid}")
traci.vehicle.remove(str(vid), 2)
[docs]def prune_vehicles(keep_vids: list):
"""
Prunes vehicles from the GUI.
Parameters
----------
keep_vids : list
The ids of the vehicle that should be kept
"""
import traci # noqa C415
for vid in set(map(int, traci.vehicle.getIDList())) - set(keep_vids):
remove_gui_vehicle(vid)
[docs]def close_gui():
"""
Close the GUI.
"""
import traci # noqa C415
traci.close(False)
[docs]def draw_ramps(road_length: int, interval: int, labels: bool):
"""
Draws on-/off-ramps in the GUI.
Parameters
----------
road_length : int
The length of the road in m
interval : int
The ramp interval in m
labels : bool
Whether to draw ramp labels
"""
y = 241
color = (0, 0, 0)
width = 4
height = 150
import traci # noqa C415
for x in range(0, road_length + 1, interval):
traci.polygon.add(
f"ramp-{x}",
[
(x - width / 2, y), # top left
(x + width / 2, y), # top right
(x + width / 2, y - height), # bottom right
(x - width / 2, y - height), # bottom left
],
color,
fill=True,
)
if labels:
traci.poi.add(
f"Ramp at {x}m",
x=x,
y=y - height - 10,
color=(51, 128, 51),
)
[docs]def draw_road_end(road_length: int, label: bool):
"""
Draws the end of the road in the GUI.
Parameters
----------
road_length : int
The length of the road in m
label : bool
Whether to draw a label
"""
y_top = 340
y_bottom = 241
width = 4
color = (255, 0, 0)
import traci # noqa C415
traci.polygon.add(
"road-end",
[
(road_length - width / 2, y_bottom), # bottom left
(road_length + width / 2, y_bottom), # bottom right
(road_length + width / 2, y_top), # top right
(road_length - width / 2, y_top), # top left
],
color,
fill=True,
layer=3,
)
if label:
traci.poi.add(
"Road End",
x=road_length + 50,
y=300,
color=(51, 128, 51),
)
[docs]def draw_infrastructures(infrastructures: list, labels: bool):
"""
Draws infrastructures in the GUI.
Parameters
----------
infrastructures : list
The list of infrastructure objects to add
labels : bool
Whether to draw infrastructure labels
"""
y = 280
width = 20
color = (0, 0, 255)
import traci # noqa C415
for infrastructure in infrastructures:
from .infrastructure import Infrastructure
assert isinstance(infrastructure, Infrastructure)
iid = str(infrastructure.iid)
position = infrastructure.position
# add infrastructure
if iid not in traci.polygon.getIDList():
traci.polygon.add(
f"rsu-{iid}",
[
(position - width / 2, y), # bottom left
(position + width / 2, y), # bottom right
(position + width / 2, y + width), # top right
(position - width / 2, y + width), # top left
],
color,
fill=True,
)
if labels:
traci.poi.add(
f"RSU {iid}",
x=position,
y=y + width + 10,
color=(51, 128, 51),
)