Files
StarPilot/selfdrive/ui/qt/onroad/annotated_camera.cc
T
2025-01-28 12:53:28 -07:00

1303 lines
52 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "selfdrive/ui/qt/onroad/annotated_camera.h"
#include <QPainter>
#include <algorithm>
#include <cmath>
#include "common/swaglog.h"
#include "selfdrive/ui/qt/onroad/buttons.h"
#include "selfdrive/ui/qt/util.h"
// Window that shows camera view and variety of info drawn on top
AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* parent) : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraWidget("camerad", type, true, parent) {
pm = std::make_unique<PubMaster, const std::initializer_list<const char *>>({"uiDebug"});
main_layout = new QVBoxLayout(this);
main_layout->setMargin(UI_BORDER_SIZE);
main_layout->setSpacing(0);
QHBoxLayout *buttons_layout = new QHBoxLayout();
buttons_layout->setSpacing(0);
// Neokii screen recorder
screenRecorder = new ScreenRecorder(this);
buttons_layout->addWidget(screenRecorder);
experimental_btn = new ExperimentalButton(this);
buttons_layout->addWidget(experimental_btn);
QVBoxLayout *top_right_layout = new QVBoxLayout();
top_right_layout->setSpacing(0);
top_right_layout->addLayout(buttons_layout);
pedal_icons = new PedalIcons(this);
top_right_layout->addWidget(pedal_icons, 0, Qt::AlignRight);
main_layout->addLayout(top_right_layout, 0);
main_layout->setAlignment(top_right_layout, Qt::AlignTop | Qt::AlignRight);
map_settings_btn = new MapSettingsButton(this);
main_layout->addWidget(map_settings_btn, 0, Qt::AlignBottom | Qt::AlignRight);
dm_img = loadPixmap("../assets/img_driver_face.png", {img_size + 5, img_size + 5});
// Initialize FrogPilot widgets
initializeFrogPilotWidgets();
}
void AnnotatedCameraWidget::updateState(int alert_height, const UIState &s) {
const int SET_SPEED_NA = 255;
const SubMaster &sm = *(s.sm);
const bool cs_alive = sm.alive("controlsState");
const bool nav_alive = sm.alive("navInstruction") && sm["navInstruction"].getValid();
const auto cs = sm["controlsState"].getControlsState();
const auto car_state = sm["carState"].getCarState();
const auto nav_instruction = sm["navInstruction"].getNavInstruction();
// Handle older routes where vCruiseCluster is not set
float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster();
setSpeed = cs_alive ? v_cruise : SET_SPEED_NA;
is_cruise_set = setSpeed > 0 && (int)setSpeed != SET_SPEED_NA;
if (is_cruise_set && !s.scene.is_metric) {
setSpeed *= KM_TO_MILE;
}
// Handle older routes where vEgoCluster is not set
v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0;
float v_ego = v_ego_cluster_seen && !s.scene.use_wheel_speed ? car_state.getVEgoCluster() : car_state.getVEgo();
speed = cs_alive ? std::max<float>(0.0, v_ego) : 0.0;
speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH;
auto speed_limit_sign = nav_instruction.getSpeedLimitSign();
if (s.scene.show_speed_limits || s.scene.speed_limit_controller) {
speedLimit = slcOverridden ? s.scene.speed_limit_overridden_speed : s.scene.speed_limit;
} else {
speedLimit = nav_alive ? nav_instruction.getSpeedLimit() : 0.0;
}
speedLimit *= (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH);
if (s.scene.speed_limit_controller && !showSLCOffset && !slcOverridden && speedLimit != 0) {
speedLimit += slcSpeedLimitOffset;
}
has_us_speed_limit = (nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD) || !useViennaSLCSign && !hideSpeedLimit;
has_eu_speed_limit = (nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA) || useViennaSLCSign && !hideSpeedLimit;
is_metric = s.scene.is_metric;
speedUnit = s.scene.is_metric ? tr("km/h") : tr("mph");
hideBottomIcons = (cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE);
hideBottomIcons |= turnSignalAnimation && (turnSignalLeft || turnSignalRight) && (signalStyle == "traditional" || signalStyle == "traditional_gif");
hideBottomIcons |= bigMapOpen;
status = s.status;
// update engageability/experimental mode button
experimental_btn->updateState(s);
// update DM icon
auto dm_state = sm["driverMonitoringState"].getDriverMonitoringState();
dmActive = dm_state.getIsActiveMode();
rightHandDM = dm_state.getIsRHD();
// DM icon transition
dm_fade_state = std::clamp(dm_fade_state+0.2*(0.5-dmActive), 0.0, 1.0);
// hide map settings button for alerts and flip for right hand DM
if (map_settings_btn->isEnabled()) {
map_settings_btn->setVisible(!hideBottomIcons && !hideMapIcon);
main_layout->setAlignment(map_settings_btn, (rightHandDM ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignBottom);
}
// Update FrogPilot widgets
updateFrogPilotVariables(alert_height, s.scene);
}
void AnnotatedCameraWidget::drawHud(QPainter &p) {
p.save();
static QElapsedTimer pendingLimitTimer;
if (speedLimitChanged) {
if (!pendingLimitTimer.isValid()) {
pendingLimitTimer.start();
}
} else {
pendingLimitTimer.invalidate();
}
QPen pendingLimitPenColor = pendingLimitTimer.isValid() && pendingLimitTimer.elapsed() % 1000 <= 500 ? QPen(redColor(), 6) : QPen(blackColor(), 6);
// Header gradient
QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT);
bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45));
bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0));
p.fillRect(0, 0, width(), UI_HEADER_HEIGHT, bg);
QString mtscSpeedStr = (mtscSpeed > 1) ? QString::number(std::nearbyint(mtscSpeed)) + speedUnit : "";
QString newSpeedLimitStr = (unconfirmedSpeedLimit > 1) ? QString::number(std::nearbyint(unconfirmedSpeedLimit)) : "";
QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : "";
QString speedLimitOffsetStr = (slcSpeedLimitOffset == 0) ? "" : QString::number(slcSpeedLimitOffset, 'f', 0).prepend((slcSpeedLimitOffset > 0) ? "+" : "");
QString speedStr = QString::number(std::nearbyint(speed));
QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed)) : "";
QString vtscSpeedStr = (vtscSpeed > 1) ? QString::number(std::nearbyint(vtscSpeed)) + speedUnit : "";
// Draw outer box + border to contain set speed and speed limit
const int sign_margin = 12;
const int us_sign_height = 186;
const int eu_sign_size = 176;
const QSize default_size = {172, 204};
QSize set_speed_size = default_size;
if (is_metric || has_eu_speed_limit) set_speed_size.rwidth() = 200;
if (has_us_speed_limit && speedLimitStr.size() >= 3) set_speed_size.rwidth() = 223;
if (has_us_speed_limit) set_speed_size.rheight() += us_sign_height + sign_margin;
else if (has_eu_speed_limit) set_speed_size.rheight() += eu_sign_size + sign_margin;
int top_radius = 32;
int bottom_radius = has_eu_speed_limit ? 100 : 32;
QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size);
if (trafficModeActive) {
p.setPen(QPen(redColor(), 10));
} else {
p.setPen(QPen(whiteColor(75), 6));
}
p.setBrush(blackColor(166));
drawRoundedRect(p, set_speed_rect, top_radius, top_radius, bottom_radius, bottom_radius);
QColor max_color = QColor(0x80, 0xd8, 0xa6, 0xff);
QColor set_speed_color = whiteColor();
if (!hideMaxSpeed) {
// Draw MAX
if (is_cruise_set) {
if (status == STATUS_DISENGAGED) {
max_color = whiteColor();
} else if (status == STATUS_OVERRIDE) {
max_color = QColor(0x91, 0x9b, 0x95, 0xff);
} else if (speedLimit > 0) {
auto interp_color = [=](QColor c1, QColor c2, QColor c3) {
return speedLimit > 0 ? interpColor(setSpeed, {speedLimit + 5, speedLimit + 15, speedLimit + 25}, {c1, c2, c3}) : c1;
};
max_color = interp_color(max_color, QColor(0xff, 0xe4, 0xbf), QColor(0xff, 0xbf, 0xbf));
set_speed_color = interp_color(set_speed_color, QColor(0xff, 0x95, 0x00), QColor(0xff, 0x00, 0x00));
}
} else {
max_color = QColor(0xa6, 0xa6, 0xa6, 0xff);
set_speed_color = QColor(0x72, 0x72, 0x72, 0xff);
}
p.setFont(InterFont(40, QFont::DemiBold));
p.setPen(max_color);
p.drawText(set_speed_rect.adjusted(0, 27, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("MAX"));
p.setFont(InterFont(90, QFont::Bold));
p.setPen(set_speed_color);
p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr);
}
if (!speedLimitChanged && is_cruise_set && (setSpeed - mtscSpeed > 1 || setSpeed - vtscSpeed > 1) && !hideCSCUI) {
std::function<void(const QRect&, const QString&, bool)> drawCurveSpeedControl = [&](const QRect &rect, const QString &speedStr, bool isMtsc) {
if (isMtsc && !vtscControllingCurve) {
p.setPen(QPen(greenColor(), 10));
p.setBrush(greenColor(166));
p.setFont(InterFont(45, QFont::Bold));
} else if (!isMtsc && vtscControllingCurve) {
p.setPen(QPen(redColor(), 10));
p.setBrush(redColor(166));
p.setFont(InterFont(45, QFont::Bold));
} else {
p.setPen(QPen(blackColor(), 10));
p.setBrush(blackColor(166));
p.setFont(InterFont(35, QFont::DemiBold));
}
p.drawRoundedRect(rect, 24, 24);
p.setPen(QPen(whiteColor(), 6));
p.drawText(rect.adjusted(20, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, speedStr);
};
QRect curveSpeedRect(QPoint(set_speed_rect.right() + 25, set_speed_rect.top()), QSize(default_size.width() * 1.25, default_size.width() * 1.25));
QPixmap scaledCurveSpeedIcon = (leftCurve ? curveSpeedLeftIcon : curveSpeedRightIcon).scaled(curveSpeedRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
p.setOpacity(1.0);
p.drawPixmap(curveSpeedRect, scaledCurveSpeedIcon);
if (mtscEnabled) {
QRect mtscRect(curveSpeedRect.topLeft() + QPoint(0, curveSpeedRect.height() + 10), QSize(curveSpeedRect.width(), vtscControllingCurve ? 50 : 100));
drawCurveSpeedControl(mtscRect, mtscSpeedStr, true);
if (vtscEnabled) {
QRect vtscRect(mtscRect.topLeft() + QPoint(0, mtscRect.height() + 20), QSize(mtscRect.width(), vtscControllingCurve ? 100 : 50));
drawCurveSpeedControl(vtscRect, vtscSpeedStr, false);
}
} else if (vtscEnabled) {
QRect vtscRect(curveSpeedRect.topLeft() + QPoint(0, curveSpeedRect.height() + 10), QSize(curveSpeedRect.width(), 150));
drawCurveSpeedControl(vtscRect, vtscSpeedStr, false);
}
}
const QRect sign_rect = set_speed_rect.adjusted(sign_margin, default_size.height(), -sign_margin, -sign_margin);
p.save();
// US/Canada (MUTCD style) sign
if (has_us_speed_limit) {
p.setPen(Qt::NoPen);
p.setBrush(whiteColor());
p.drawRoundedRect(sign_rect, 24, 24);
p.setPen(QPen(blackColor(), 6));
p.drawRoundedRect(sign_rect.adjusted(9, 9, -9, -9), 16, 16);
p.save();
p.setOpacity(slcOverridden ? 0.25 : 1.0);
if (showSLCOffset && !slcOverridden) {
p.setFont(InterFont(28, QFont::DemiBold));
p.drawText(sign_rect.adjusted(0, 22, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT"));
p.setFont(InterFont(70, QFont::Bold));
p.drawText(sign_rect.adjusted(0, 51, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitStr);
p.setFont(InterFont(50, QFont::DemiBold));
p.drawText(sign_rect.adjusted(0, 120, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitOffsetStr);
} else {
p.setFont(InterFont(28, QFont::DemiBold));
p.drawText(sign_rect.adjusted(0, 22, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("SPEED"));
p.drawText(sign_rect.adjusted(0, 51, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT"));
p.setFont(InterFont(70, QFont::Bold));
p.drawText(sign_rect.adjusted(0, 85, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitStr);
}
p.restore();
if (speedLimitChanged && !(setSpeed - mtscSpeed > 1 || setSpeed - vtscSpeed > 1)) {
QRect new_sign_rect(sign_rect.translated(sign_rect.width() + 25, 0));
new_sign_rect.setWidth(newSpeedLimitStr.size() >= 3 ? 200 : 175);
newSpeedLimitRect = new_sign_rect;
newSpeedLimitRect.setWidth(new_sign_rect.width());
newSpeedLimitRect.setHeight(new_sign_rect.height());
p.setPen(Qt::NoPen);
p.setBrush(whiteColor());
p.drawRoundedRect(new_sign_rect, 24, 24);
p.setPen(pendingLimitPenColor);
p.drawRoundedRect(new_sign_rect.adjusted(9, 9, -9, -9), 16, 16);
p.setFont(InterFont(28, QFont::DemiBold));
p.drawText(new_sign_rect.adjusted(0, 22, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("PENDING"));
p.drawText(new_sign_rect.adjusted(0, 51, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT"));
p.setFont(InterFont(70, QFont::Bold));
p.drawText(new_sign_rect.adjusted(0, 85, 0, 0), Qt::AlignTop | Qt::AlignHCenter, newSpeedLimitStr);
}
}
// EU (Vienna style) sign
if (has_eu_speed_limit) {
p.setPen(Qt::NoPen);
p.setBrush(whiteColor());
p.drawEllipse(sign_rect);
p.setPen(QPen(Qt::red, 20));
p.drawEllipse(sign_rect.adjusted(16, 16, -16, -16));
p.setOpacity(slcOverridden ? 0.25 : 1.0);
p.setPen(blackColor());
if (showSLCOffset) {
p.setFont(InterFont((speedLimitStr.size() >= 3) ? 60 : 70, QFont::Bold));
p.drawText(sign_rect.adjusted(0, -25, 0, 0), Qt::AlignCenter, speedLimitStr);
p.setFont(InterFont(40, QFont::DemiBold));
p.drawText(sign_rect.adjusted(0, 100, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitOffsetStr);
} else {
p.setFont(InterFont((speedLimitStr.size() >= 3) ? 60 : 70, QFont::Bold));
p.drawText(sign_rect, Qt::AlignCenter, speedLimitStr);
}
if (speedLimitChanged) {
QRect new_sign_rect(sign_rect.translated(sign_rect.width() + 25, 0));
p.setPen(Qt::NoPen);
p.setBrush(whiteColor());
p.drawEllipse(new_sign_rect);
p.setPen(QPen(Qt::red, 20));
p.drawEllipse(new_sign_rect.adjusted(16, 16, -16, -16));
p.setOpacity(1.0);
p.setPen(pendingLimitPenColor);
p.setFont(InterFont((newSpeedLimitStr.size() >= 3) ? 60 : 70, QFont::Bold));
p.drawText(new_sign_rect, Qt::AlignCenter, newSpeedLimitStr);
}
}
if (speedLimitSources && (has_eu_speed_limit || has_us_speed_limit)) {
std::function<void(QRect&, const QPixmap&, const QString&, double)> drawSource = [&](QRect &rect, const QPixmap &icon, const QString &title, double speedLimitValue) {
if (speedLimitSource == title.toStdString()) {
p.setPen(QPen(redColor(), 10));
p.setBrush(redColor(166));
p.setFont(InterFont(35, QFont::Bold));
} else {
p.setPen(QPen(blackColor(), 10));
p.setBrush(blackColor(166));
p.setFont(InterFont(35, QFont::DemiBold));
}
QRect iconRect(rect.x() + 20, rect.y() + (rect.height() - img_size / 4) / 2, img_size / 4, img_size / 4);
QPixmap scaledIcon = icon.scaled(iconRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
QString speedText;
if (speedLimitValue > 1) {
speedText = QString::number(std::nearbyint(speedLimitValue)) + " " + speedUnit;
} else {
speedText = "N/A";
}
QString fullText = tr(title.toUtf8().constData()) + " - " + speedText;
p.setOpacity(1.0);
p.drawRoundedRect(rect, 24, 24);
p.drawPixmap(iconRect, scaledIcon);
p.setPen(QPen(whiteColor(), 6));
QRect textRect(iconRect.right() + 10, rect.y(), rect.width() - iconRect.width() - 30, rect.height());
p.drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, fullText);
};
QRect dashboardRect(sign_rect.x() - sign_margin, sign_rect.y() + sign_rect.height() + 45, 500, 60);
QRect mapDataRect(dashboardRect.x(), dashboardRect.y() + dashboardRect.height() + 15, 500, 60);
QRect navigationRect(mapDataRect.x(), mapDataRect.y() + mapDataRect.height() + 15, 500, 60);
QRect upcomingLimitRect(navigationRect.x(), navigationRect.y() + navigationRect.height() + 15, 500, 60);
drawSource(dashboardRect, dashboardIcon, "Dashboard", dashboardSpeedLimit);
drawSource(mapDataRect, mapDataIcon, "Map Data", mapsSpeedLimit);
drawSource(navigationRect, navigationIcon, "Navigation", navigationSpeedLimit);
drawSource(upcomingLimitRect, upcomingMapsIcon, "Upcoming", upcomingSpeedLimit);
}
p.restore();
// current speed
if (!(bigMapOpen || hideSpeed)) {
if (standstillDuration > 1) {
float transition = qBound(0.0f, standstillDuration / 120.0f, 1.0f);
QColor start, end;
if (standstillDuration <= 60) {
start = end = bg_colors[STATUS_ENGAGED];
} else if (standstillDuration <= 90) {
start = bg_colors[STATUS_ENGAGED];
end = bg_colors[STATUS_CONDITIONAL_OVERRIDDEN];
transition = (standstillDuration - 60) / 30.0f;
} else if (standstillDuration <= 120) {
start = bg_colors[STATUS_CONDITIONAL_OVERRIDDEN];
end = bg_colors[STATUS_TRAFFIC_MODE_ACTIVE];
transition = (standstillDuration - 90) / 30.0f;
} else {
start = end = bg_colors[STATUS_TRAFFIC_MODE_ACTIVE];
transition = 0.0f;
}
QColor blendedColor(
start.red() + transition * (end.red() - start.red()),
start.green() + transition * (end.green() - start.green()),
start.blue() + transition * (end.blue() - start.blue())
);
p.setPen(QPen(blendedColor));
int minutes = standstillDuration / 60;
int seconds = standstillDuration % 60;
p.setFont(InterFont(176, QFont::Bold));
drawText(p, rect().center().x(), 210, minutes == 1 ? "1 minute" : QString("%1 minutes").arg(minutes), 255, true);
p.setFont(InterFont(66));
drawText(p, rect().center().x(), 290, QString("%1 seconds").arg(seconds));
} else {
p.setFont(InterFont(176, QFont::Bold));
drawText(p, rect().center().x(), 210, speedStr);
p.setFont(InterFont(66));
drawText(p, rect().center().x(), 290, speedUnit, 200);
}
}
p.restore();
}
void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &text, int alpha, bool overridePen) {
QRect real_rect = p.fontMetrics().boundingRect(text);
real_rect.moveCenter({x, y - real_rect.height() / 2});
if (!overridePen) {
p.setPen(QColor(0xff, 0xff, 0xff, alpha));
}
p.drawText(real_rect.x(), real_rect.bottom(), text);
}
void AnnotatedCameraWidget::initializeGL() {
CameraWidget::initializeGL();
qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION));
qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR));
qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER));
qInfo() << "OpenGL language version:" << QString((const char*)glGetString(GL_SHADING_LANGUAGE_VERSION));
prev_draw_t = millis_since_boot();
setBackgroundColor(bg_colors[STATUS_DISENGAGED]);
}
void AnnotatedCameraWidget::updateFrameMat() {
CameraWidget::updateFrameMat();
UIState *s = uiState();
int w = width(), h = height();
s->fb_w = w;
s->fb_h = h;
// Apply transformation such that video pixel coordinates match video
// 1) Put (0, 0) in the middle of the video
// 2) Apply same scaling as video
// 3) Put (0, 0) in top left corner of video
s->car_space_transform.reset();
s->car_space_transform.translate(w / 2 - x_offset, h / 2 - y_offset)
.scale(zoom, zoom)
.translate(-intrinsic_matrix.v[2], -intrinsic_matrix.v[5]);
}
void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s, float v_ego) {
painter.save();
const UIScene &scene = s->scene;
SubMaster &sm = *(s->sm);
// lanelines
for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) {
if (useStockColors) {
painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp<float>(scene.lane_line_probs[i], 0.0, 0.7)));
} else {
painter.setBrush(scene.lane_lines_color);
}
painter.drawPolygon(scene.lane_line_vertices[i]);
}
// road edges
for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) {
painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp<float>(1.0 - scene.road_edge_stds[i], 0.0, 1.0)));
painter.drawPolygon(scene.road_edge_vertices[i]);
}
// paint path
QLinearGradient bg(0, height(), 0, 0);
if (experimentalMode || scene.acceleration_path || scene.rainbow_path) {
// The first half of track_vertices are the points for the right side of the path
// and the indices match the positions of accel from uiPlan
const auto &acceleration_const = sm["uiPlan"].getUiPlan().getAccel();
const int max_len = std::min<int>(scene.track_vertices.length() / 2, acceleration_const.size());
// Copy of the acceleration vector
std::vector<float> acceleration;
acceleration.reserve(acceleration_const.size());
for (size_t i = 0; i < acceleration_const.size(); ++i) {
acceleration.push_back(acceleration_const[i]);
}
const float hue_shift_speed = 0.5; // Adjust this value to control the speed of the rainbow scroll
static float hue_base = 0.0; // Base hue that changes over time
hue_base = fmod(hue_base + v_ego * hue_shift_speed, 360.0); // Update base hue to create scrolling effect
for (int i = 0; i < max_len; ++i) {
// Some points are out of frame
int track_idx = max_len - i - 1; // flip idx to start from bottom right
if (scene.track_vertices[track_idx].y() < 0 || scene.track_vertices[track_idx].y() > height()) continue;
// Flip so 0 is bottom of frame
float lin_grad_point = (height() - scene.track_vertices[track_idx].y()) / height();
float acceleration_abs = fabs(acceleration[i]);
if (acceleration_abs < 0.25 && scene.rainbow_path) {
float saturation = util::map_val(acceleration_abs, 0.0f, 1.0f, 0.6f, 0.8f);
float lightness = util::map_val(acceleration_abs, 0.0f, 1.0f, 0.7f, 0.5f);
float alpha = util::map_val(acceleration_abs, 0.0f, 1.0f, 0.5f, 0.8f);
float perspective_factor = lin_grad_point;
float rainbow_height = 0.1 + 0.4 * perspective_factor;
for (int j = 0; j <= 50; ++j) {
float color_position = static_cast<float>(j) / 50.0;
if (color_position >= lin_grad_point - rainbow_height / 2 && color_position <= lin_grad_point + rainbow_height / 2) {
float hue = fmod(hue_base + color_position * 360.0, 360.0);
QColor rainbow_color = QColor::fromHslF(hue / 360.0, saturation, lightness, alpha);
bg.setColorAt(color_position, rainbow_color);
}
}
} else if (acceleration_abs < 0.25 && !useStockColors) {
QColor color = scene.path_color;
bg.setColorAt(0.0f, color);
color.setAlphaF(0.5f);
bg.setColorAt(0.5f, color);
color.setAlphaF(0.1f);
bg.setColorAt(1.0f, color);
} else {
// speed up: 120, slow down: 0
float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0);
// FIXME: painter.drawPolygon can be slow if hue is not rounded
path_hue = int(path_hue * 100 + 0.5) / 100;
float saturation = fmin(fabs(acceleration[i] * 1.5), 1);
float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey
float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade
bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha));
// Skip a point, unless next is last
i += (i + 2) < max_len ? 1 : 0;
}
}
} else {
bg.setColorAt(0.0, QColor::fromHslF(148 / 360., 0.94, 0.51, 0.4));
bg.setColorAt(0.5, QColor::fromHslF(112 / 360., 1.0, 0.68, 0.35));
bg.setColorAt(1.0, QColor::fromHslF(112 / 360., 1.0, 0.68, 0.0));
}
painter.setBrush(bg);
painter.drawPolygon(scene.track_vertices);
if (scene.show_stopping_point && scene.red_light && speed > 1 && !(conditionalStatus == 1 || conditionalStatus == 3 || conditionalStatus == 5)) {
QPointF last_point = scene.track_vertices.last();
QPointF adjusted_point = last_point - QPointF(stopSignImg.width() / 2, stopSignImg.height());
painter.drawPixmap(adjusted_point, stopSignImg);
if (scene.show_stopping_point_metrics) {
QFont font = InterFont(35, QFont::DemiBold);
QString text = QString::number(std::nearbyint(modelLength * distanceConversion)) + leadDistanceUnit;
int text_width = QFontMetrics(font).horizontalAdvance(text);
QPointF text_position = last_point - QPointF(text_width / 2, stopSignImg.height() + 35);
painter.save();
painter.setFont(font);
painter.setPen(Qt::white);
painter.drawText(text_position, text);
painter.restore();
}
}
// Paint blindspot path
if (scene.blind_spot_path) {
QLinearGradient bs(0, height(), 0, 0);
bs.setColorAt(0.0f, QColor::fromHslF(0 / 360.0f, 0.75f, 0.5f, 0.6f));
bs.setColorAt(0.5f, QColor::fromHslF(0 / 360.0f, 0.75f, 0.5f, 0.4f));
bs.setColorAt(1.0f, QColor::fromHslF(0 / 360.0f, 0.75f, 0.5f, 0.2f));
painter.setBrush(bs);
if (blindSpotLeft) {
painter.drawPolygon(scene.track_adjacent_vertices[4]);
}
if (blindSpotRight) {
painter.drawPolygon(scene.track_adjacent_vertices[5]);
}
}
// Paint adjacent lane paths
if ((scene.adjacent_path || scene.adjacent_path_metrics) && v_ego >= scene.minimum_lane_change_speed) {
QLinearGradient ap(0, height(), 0, 0);
std::function<void(float)> setAdjacentPathColors = [&](float hue) {
ap.setColorAt(0.0f, QColor::fromHslF(hue / 360.0f, 0.75f, 0.5f, 0.6f));
ap.setColorAt(0.5f, QColor::fromHslF(hue / 360.0f, 0.75f, 0.5f, 0.4f));
ap.setColorAt(1.0f, QColor::fromHslF(hue / 360.0f, 0.75f, 0.5f, 0.2f));
};
std::function<void(const QPolygonF&, float, bool)> drawAdjacentLane = [&](const QPolygonF &lane, float laneWidth, bool isBlindSpot) {
if (isBlindSpot) {
setAdjacentPathColors(0.0f);
} else {
float hue = 120.0f * (1 - fmin(fabs(laneWidth - laneDetectionWidth) / (laneDetectionWidth / 2), 1));
setAdjacentPathColors(hue);
}
painter.setBrush(ap);
painter.drawPolygon(lane);
if (scene.adjacent_path_metrics) {
painter.setFont(InterFont(30, QFont::DemiBold));
painter.setPen(Qt::white);
QString text = isBlindSpot ? tr("Vehicle in blind spot") : QString::number(laneWidth * distanceConversion, 'f', 2) + leadDistanceUnit;
painter.drawText(lane.boundingRect(), Qt::AlignCenter, text);
painter.setPen(Qt::NoPen);
}
};
drawAdjacentLane(scene.track_adjacent_vertices[4], scene.lane_width_left, blindSpotLeft);
drawAdjacentLane(scene.track_adjacent_vertices[5], scene.lane_width_right, blindSpotRight);
}
// Paint path edges
QLinearGradient pe(0, height(), 0, 0);
std::function<void(QLinearGradient&, const QColor&)> setPathEdgeColors = [&](QLinearGradient &gradient, const QColor &baseColor) {
gradient.setColorAt(0.0f, baseColor);
QColor color = baseColor;
color.setAlphaF(0.5f);
gradient.setColorAt(0.5f, color);
color.setAlphaF(0.1f);
gradient.setColorAt(1.0f, color);
};
if (scene.always_on_lateral_active) {
setPathEdgeColors(pe, bg_colors[STATUS_ALWAYS_ON_LATERAL_ACTIVE]);
} else if (conditionalStatus == 1 || conditionalStatus == 3 || conditionalStatus == 5) {
setPathEdgeColors(pe, bg_colors[STATUS_CONDITIONAL_OVERRIDDEN]);
} else if (experimentalMode) {
setPathEdgeColors(pe, bg_colors[STATUS_EXPERIMENTAL_MODE_ACTIVE]);
} else if (trafficModeActive) {
setPathEdgeColors(pe, bg_colors[STATUS_TRAFFIC_MODE_ACTIVE]);
} else if (modelLength > scene.upcoming_maneuver_distance && scene.upcoming_maneuver_distance > 1) {
setPathEdgeColors(pe, bg_colors[STATUS_NAVIGATION_ACTIVE]);
} else if (!useStockColors) {
setPathEdgeColors(pe, scene.path_edges_color);
} else {
pe.setColorAt(0.0f, QColor::fromHslF(148 / 360.0f, 0.94f, 0.51f, 1.0f));
pe.setColorAt(0.5f, QColor::fromHslF(112 / 360.0f, 1.00f, 0.68f, 0.5f));
pe.setColorAt(1.0f, QColor::fromHslF(112 / 360.0f, 1.00f, 0.68f, 0.1f));
}
QPainterPath path;
path.addPolygon(scene.track_vertices);
path.addPolygon(scene.track_edge_vertices);
painter.setBrush(pe);
painter.drawPath(path);
painter.restore();
}
void AnnotatedCameraWidget::drawDriverState(QPainter &painter, const UIState *s) {
const UIScene &scene = s->scene;
painter.save();
// base icon
int offset = UI_BORDER_SIZE + btn_size / 2;
int x = rightHandDM ? width() - offset : offset;
if (rightHandDM && map_settings_btn->isEnabled() && !hideMapIcon) {
x -= 250;
} else if (onroadDistanceButton) {
x += 250;
}
int y = height() - offset;
dmIconPosition.setX(x);
dmIconPosition.setY(y);
float opacity = dmActive ? 0.65 : 0.2;
drawIcon(painter, dmIconPosition, dm_img, blackColor(70), opacity);
// face
QPointF face_kpts_draw[std::size(default_face_kpts_3d)];
float kp;
for (int i = 0; i < std::size(default_face_kpts_3d); ++i) {
kp = (scene.face_kpts_draw[i].v[2] - 8) / 120 + 1.0;
face_kpts_draw[i] = QPointF(scene.face_kpts_draw[i].v[0] * kp + x, scene.face_kpts_draw[i].v[1] * kp + y);
}
painter.setPen(QPen(QColor::fromRgbF(1.0, 1.0, 1.0, opacity), 5.2, Qt::SolidLine, Qt::RoundCap));
painter.drawPolyline(face_kpts_draw, std::size(default_face_kpts_3d));
// tracking arcs
const int arc_l = 133;
const float arc_t_default = 6.7;
const float arc_t_extend = 12.0;
QColor arc_color = QColor::fromRgbF(0.545 - 0.445 * s->engaged(),
0.545 + 0.4 * s->engaged(),
0.545 - 0.285 * s->engaged(),
0.4 * (1.0 - dm_fade_state));
float delta_x = -scene.driver_pose_sins[1] * arc_l / 2;
float delta_y = -scene.driver_pose_sins[0] * arc_l / 2;
painter.setPen(QPen(arc_color, arc_t_default+arc_t_extend*fmin(1.0, scene.driver_pose_diff[1] * 5.0), Qt::SolidLine, Qt::RoundCap));
painter.drawArc(QRectF(std::fmin(x + delta_x, x), y - arc_l / 2, fabs(delta_x), arc_l), (scene.driver_pose_sins[1]>0 ? 90 : -90) * 16, 180 * 16);
painter.setPen(QPen(arc_color, arc_t_default+arc_t_extend*fmin(1.0, scene.driver_pose_diff[0] * 5.0), Qt::SolidLine, Qt::RoundCap));
painter.drawArc(QRectF(x - arc_l / 2, std::fmin(y + delta_y, y), arc_l, fabs(delta_y)), (scene.driver_pose_sins[0]>0 ? 0 : 180) * 16, 180 * 16);
painter.restore();
}
void AnnotatedCameraWidget::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, float v_ego, const QColor &lead_marker_color, bool adjacent) {
painter.save();
const float speedBuff = 10.;
const float leadBuff = adjacent ? 100. : 40.;
const float d_rel = lead_data.getDRel() + (adjacent ? fabs(lead_data.getYRel()) : 0);
const float v_rel = lead_data.getVRel();
float fillAlpha = 0;
if (d_rel < leadBuff) {
fillAlpha = 255 * (1.0 - (d_rel / leadBuff));
if (v_rel < 0) {
fillAlpha += 255 * (-1 * (v_rel / speedBuff));
}
fillAlpha = (int)(fmin(fillAlpha, 255));
}
float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), adjacent ? 10.0f : 15.0f, adjacent ? 20.0f : 30.0f) * 2.35;
float x = std::clamp((float)vd.x(), 0.f, width() - sz / 2);
float y = std::fmin(height() - sz * .6, (float)vd.y());
float g_xo = sz / 5;
float g_yo = sz / 10;
QPointF glow[] = {{x + (sz * 1.35) + g_xo, y + sz + g_yo}, {x, y - g_yo}, {x - (sz * 1.35) - g_xo, y + sz + g_yo}};
painter.setBrush(QColor(218, 202, 37, 255));
painter.drawPolygon(glow, std::size(glow));
// chevron
QPointF chevron[] = {{x + (sz * 1.25), y + sz}, {x, y}, {x - (sz * 1.25), y + sz}};
if (!adjacent && useStockColors) {
painter.setBrush(redColor(fillAlpha));
} else {
painter.setBrush(QColor(lead_marker_color.red(), lead_marker_color.green(), lead_marker_color.blue(), fillAlpha));
}
painter.drawPolygon(chevron, std::size(chevron));
if (leadInfo) {
float lead_speed = std::max(v_rel + v_ego, 0.0f);
painter.setPen(Qt::white);
painter.setFont(InterFont(35, QFont::Bold));
QString text;
if (adjacent) {
text = QString("%1 %2 | %3 %4")
.arg(qRound(d_rel * distanceConversion))
.arg(leadDistanceUnit)
.arg(qRound(lead_speed * speedConversionMetrics))
.arg(leadSpeedUnit);
} else {
text = QString("%1 %2 (%3) | %4 %5 | %6%7")
.arg(qRound(d_rel * distanceConversion))
.arg(leadDistanceUnit)
.arg(QString("Desired: %1").arg(desiredFollow * distanceConversion))
.arg(qRound(lead_speed * speedConversionMetrics))
.arg(leadSpeedUnit)
.arg(QString::number(std::max(d_rel / std::max(v_ego, 1.0f), 1.0f), 'f', 2))
.arg("s");
}
QFontMetrics metrics(painter.font());
int middle_x = (chevron[2].x() + chevron[0].x()) / 2;
int textHeight = metrics.height();
int textWidth = metrics.horizontalAdvance(text);
int text_x = middle_x - textWidth / 2;
int text_y = chevron[0].y() + textHeight + 5;
if (!adjacent) {
lead_x = x + text_x + textWidth;
lead_y = y + text_y + textHeight;
}
if (!adjacent || fabs((x + text_x + textWidth) - lead_x) >= textWidth * 1.25 || fabs((y + text_y + textHeight) - lead_y) >= textHeight * 2) {
painter.drawText(text_x, text_y, text);
}
}
painter.restore();
}
void AnnotatedCameraWidget::paintGL() {
}
void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) {
UIState *s = uiState();
SubMaster &sm = *(s->sm);
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
const double start_draw_t = millis_since_boot();
const cereal::ModelDataV2::Reader &model = sm["modelV2"].getModelV2();
const float v_ego = sm["carState"].getCarState().getVEgo();
// draw camera frame
{
std::lock_guard lk(frame_lock);
if (frames.empty()) {
if (skip_frame_count > 0) {
skip_frame_count--;
qDebug() << "skipping frame, not ready";
return;
}
} else {
// skip drawing up to this many frames if we're
// missing camera frames. this smooths out the
// transitions from the narrow and wide cameras
skip_frame_count = 5;
}
// Wide or narrow cam dependent on speed
bool has_wide_cam = available_streams.count(VISION_STREAM_WIDE_ROAD);
if (has_wide_cam && cameraView == 0) {
if ((v_ego < 10) || available_streams.size() == 1) {
wide_cam_requested = true;
} else if (v_ego > 15) {
wide_cam_requested = false;
}
wide_cam_requested = wide_cam_requested && experimentalMode;
// for replay of old routes, never go to widecam
wide_cam_requested = wide_cam_requested && s->scene.calibration_wide_valid;
}
CameraWidget::setStreamType(cameraView == 1 ? VISION_STREAM_DRIVER :
cameraView == 3 || wide_cam_requested ? VISION_STREAM_WIDE_ROAD :
VISION_STREAM_ROAD);
s->scene.wide_cam = CameraWidget::getStreamType() == VISION_STREAM_WIDE_ROAD;
if (s->scene.calibration_valid) {
auto calib = s->scene.wide_cam ? s->scene.view_from_wide_calib : s->scene.view_from_calib;
CameraWidget::updateCalibration(calib);
} else {
CameraWidget::updateCalibration(DEFAULT_CALIBRATION);
}
painter.beginNativePainting();
CameraWidget::setFrameId(model.getFrameId());
CameraWidget::paintGL();
painter.endNativePainting();
}
painter.setPen(Qt::NoPen);
if (s->scene.world_objects_visible) {
update_model(s, model, sm["uiPlan"].getUiPlan());
drawLaneLines(painter, s, v_ego);
if (s->scene.longitudinal_control && sm.rcv_frame("radarState") > s->scene.started_frame && !s->scene.hide_lead_marker) {
auto radar_state = sm["radarState"].getRadarState();
update_leads(s, radar_state, model.getPosition());
auto lead_one = radar_state.getLeadOne();
auto lead_two = radar_state.getLeadTwo();
auto lead_left = radar_state.getLeadLeft();
auto lead_right = radar_state.getLeadRight();
auto lead_left_far = radar_state.getLeadLeftFar();
auto lead_right_far = radar_state.getLeadRightFar();
if (lead_two.getStatus()) {
drawLead(painter, lead_two, s->scene.lead_vertices[1], v_ego, s->scene.lead_marker_color);
} else if (lead_one.getStatus()) {
drawLead(painter, lead_one, s->scene.lead_vertices[0], v_ego, s->scene.lead_marker_color);
} else {
lead_x = 0;
lead_y = 0;
}
if (lead_left.getStatus()) {
drawLead(painter, lead_left, s->scene.lead_vertices[2], v_ego, blueColor(), true);
}
if (lead_right.getStatus()) {
drawLead(painter, lead_right, s->scene.lead_vertices[3], v_ego, purpleColor(), true);
}
if (lead_left_far.getStatus()) {
drawLead(painter, lead_left_far, s->scene.lead_vertices[4], v_ego, orangeColor(), true);
}
if (lead_right_far.getStatus()) {
drawLead(painter, lead_right_far, s->scene.lead_vertices[5], v_ego, redColor(), true);
}
}
}
// DMoji
if (!hideBottomIcons && (sm.rcv_frame("driverStateV2") > s->scene.started_frame)) {
update_dmonitoring(s, sm["driverStateV2"].getDriverStateV2(), dm_fade_state, rightHandDM);
drawDriverState(painter, s);
}
drawHud(painter);
double cur_draw_t = millis_since_boot();
double dt = cur_draw_t - prev_draw_t;
double fps = fps_filter.update(1. / dt * 1000);
s->scene.fps = fps;
if (fps < 15) {
LOGW("slow frame rate: %.2f fps", fps);
}
prev_draw_t = cur_draw_t;
// publish debug msg
MessageBuilder msg;
auto m = msg.initEvent().initUiDebug();
m.setDrawTimeMillis(cur_draw_t - start_draw_t);
pm->send("uiDebug", msg);
// Paint FrogPilot widgets
paintFrogPilotWidgets(painter);
}
void AnnotatedCameraWidget::showEvent(QShowEvent *event) {
CameraWidget::showEvent(event);
ui_update_params(uiState());
prev_draw_t = millis_since_boot();
// FrogPilot variables
distance_btn->updateIcon();
experimental_btn->updateIcon();
updateSignals();
}
// FrogPilot widgets
void AnnotatedCameraWidget::updateSignals() {
blindspotImages.clear();
signalImages.clear();
QDir directory("../frogpilot/assets/active_theme/signals/");
QFileInfoList allFiles = directory.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name);
bool isGif = false;
for (QFileInfo &fileInfo : allFiles) {
if (fileInfo.fileName().endsWith(".gif", Qt::CaseInsensitive)) {
QMovie movie(fileInfo.absoluteFilePath());
movie.start();
for (int frameIndex = 0; frameIndex < movie.frameCount(); ++frameIndex) {
movie.jumpToFrame(frameIndex);
QPixmap currentFrame = movie.currentPixmap();
signalImages.push_back(currentFrame);
signalImages.push_back(currentFrame.transformed(QTransform().scale(-1, 1)));
}
movie.stop();
isGif = true;
} else if (fileInfo.fileName().endsWith(".png", Qt::CaseInsensitive)) {
QVector<QPixmap> *targetList = fileInfo.fileName().contains("blindspot") ? &blindspotImages : &signalImages;
QPixmap pixmap(fileInfo.absoluteFilePath());
targetList->push_back(pixmap);
targetList->push_back(pixmap.transformed(QTransform().scale(-1, 1)));
} else {
QStringList parts = fileInfo.fileName().split('_');
if (parts.size() == 2) {
signalStyle = parts[0];
signalAnimationLength = parts[1].toInt();
}
}
}
if (!signalImages.empty()) {
QPixmap &firstImage = signalImages.front();
signalWidth = firstImage.width();
signalHeight = firstImage.height();
totalFrames = signalImages.size() / 2;
turnSignalAnimation = true;
if (isGif && signalStyle == "traditional") {
signalMovement = (this->size().width() + (signalWidth * 2)) / totalFrames;
signalStyle = "traditional_gif";
} else {
signalMovement = 0;
}
} else {
signalWidth = 0;
signalHeight = 0;
totalFrames = 0;
turnSignalAnimation = false;
}
}
void AnnotatedCameraWidget::initializeFrogPilotWidgets() {
distance_btn = new DistanceButton(this);
main_layout->addWidget(distance_btn, 0, Qt::AlignBottom | Qt::AlignLeft);
chillModeIcon = loadPixmap("../frogpilot/assets/other_images/chill_mode_icon.png", {img_size / 2, img_size / 2});
curveIcon = loadPixmap("../frogpilot/assets/other_images/curve_icon.png", {img_size / 2, img_size / 2});
curveSpeedLeftIcon = loadPixmap("../frogpilot/assets/other_images/curve_speed_left.png", {img_size, img_size});
curveSpeedRightIcon = loadPixmap("../frogpilot/assets/other_images/curve_speed_right.png", {img_size, img_size});
dashboardIcon = loadPixmap("../frogpilot/assets/other_images/dashboard_icon.png", {img_size / 2, img_size / 2});
experimentalModeIcon = loadPixmap("../assets/img_experimental.svg", {img_size / 2, img_size / 2});
leadIcon = loadPixmap("../frogpilot/assets/other_images/lead_icon.png", {img_size / 2, img_size / 2});
lightIcon = loadPixmap("../frogpilot/assets/other_images/light_icon.png", {img_size / 2, img_size / 2});
mapDataIcon = loadPixmap("../frogpilot/assets/other_images/offline_maps_icon.png", {img_size / 2, img_size / 2});
navigationIcon = loadPixmap("../frogpilot/assets/other_images/navigation_icon.png", {img_size / 2, img_size / 2});
speedIcon = loadPixmap("../frogpilot/assets/other_images/speed_icon.png", {img_size / 2, img_size / 2});
stopSignImg = loadPixmap("../frogpilot/assets/other_images/stop_sign.png", {img_size, img_size});
turnIcon = loadPixmap("../frogpilot/assets/other_images/turn_icon.png", {img_size / 2, img_size / 2});
upcomingMapsIcon = loadPixmap("../frogpilot/assets/other_images/upcoming_maps_icon.png", {img_size / 2, img_size / 2});
animationTimer = new QTimer(this);
QObject::connect(animationTimer, &QTimer::timeout, [this] {
animationFrameIndex = (animationFrameIndex + 1) % totalFrames;
});
QObject::connect(uiState(), &UIState::themeUpdated, this, &AnnotatedCameraWidget::updateSignals);
QObject::connect(uiState(), &UIState::themeUpdated, distance_btn, &DistanceButton::updateIcon);
QObject::connect(uiState(), &UIState::themeUpdated, experimental_btn, &ExperimentalButton::updateIcon);
}
void AnnotatedCameraWidget::updateFrogPilotVariables(int alert_height, const UIScene &scene) {
if (is_metric || useSI) {
accelerationUnit = tr("m/s²");
leadDistanceUnit = tr(mapOpen ? "m" : "meters");
leadSpeedUnit = useSI ? tr("m/s") : tr("kph");
accelerationConversion = 1.0f;
distanceConversion = 1.0f;
speedConversion = is_metric ? MS_TO_KPH : MS_TO_MPH;
speedConversionMetrics = useSI ? 1.0f : MS_TO_KPH;
} else {
accelerationUnit = tr("ft/s²");
leadDistanceUnit = tr(mapOpen ? "ft" : "feet");
leadSpeedUnit = tr("mph");
accelerationConversion = METER_TO_FOOT;
distanceConversion = METER_TO_FOOT;
speedConversion = MS_TO_MPH;
speedConversionMetrics = MS_TO_MPH;
}
alertHeight = alert_height;
blindSpotLeft = scene.blind_spot_left;
blindSpotRight = scene.blind_spot_right;
cameraView = scene.camera_view;
cemStatus = scene.cem_status;
conditionalStatus = scene.conditional_status;
compass = scene.compass;
desiredFollow = scene.desired_follow;
experimentalMode = scene.experimental_mode;
hideCSCUI = scene.hide_csc_ui;
hideMapIcon = scene.hide_map_icon;
hideMaxSpeed = scene.hide_max_speed;
hideSpeed = scene.hide_speed;
hideSpeedLimit = scene.hide_speed_limit;
laneDetectionWidth = scene.lane_detection_width;
leadInfo = scene.lead_metrics;
leftCurve = scene.left_curve;
mapOpen = scene.map_open;
bigMapOpen = mapOpen && scene.big_map;
modelLength = scene.model_length;
mtscEnabled = scene.mtsc_enabled;
mtscSpeed = mtscEnabled ? scene.mtsc_speed * speedConversion : setSpeed;
onroadDistanceButton = scene.onroad_distance_button;
bool enableDistanceButton = onroadDistanceButton && !hideBottomIcons;
distance_btn->setVisible(enableDistanceButton);
if (enableDistanceButton) {
distance_btn->updateState(scene);
}
bool enablePedalIcons = scene.pedals_on_ui && !bigMapOpen;
pedal_icons->setVisible(enablePedalIcons);
if (enablePedalIcons) {
pedal_icons->updateState(scene);
}
roadNameUI = scene.road_name_ui;
bool enableScreenRecorder = scene.screen_recorder && !mapOpen;
screenRecorder->setVisible(enableScreenRecorder);
if (enableScreenRecorder) {
screenRecorder->updateScreen(scene.fps, scene.started);
}
dashboardSpeedLimit = scene.dashboard_speed_limit * speedConversion;
mapsSpeedLimit = scene.speed_limit_map * speedConversion;
navigationSpeedLimit = scene.navigation_speed_limit * speedConversion;
showSLCOffset = scene.show_speed_limit_offset;
slcOverridden = scene.speed_limit_overridden;
slcSpeedLimitOffset = scene.speed_limit_offset * speedConversion;
speedLimitChanged = scene.speed_limit_changed;
speedLimitSource = scene.speed_limit_source;
speedLimitSources = scene.speed_limit_sources;
unconfirmedSpeedLimit = scene.unconfirmed_speed_limit * speedConversion;
upcomingSpeedLimit = scene.upcoming_speed_limit * speedConversion;
useViennaSLCSign = scene.speed_limit_vienna;
bool stoppedTimer = scene.stopped_timer && scene.standstill && scene.started_timer / UI_FREQ >= 10 && !mapOpen;
static QElapsedTimer standstillTimer;
if (stoppedTimer) {
if (!standstillTimer.isValid()) {
standstillTimer.start();
}
standstillDuration = standstillTimer.elapsed() / 1000.0;
} else {
standstillDuration = 0;
standstillTimer.invalidate();
}
trafficModeActive = scene.traffic_mode_active;
turnSignalLeft = scene.turn_signal_left;
turnSignalRight = scene.turn_signal_right;
useSI = scene.use_si_metrics;
useStockColors = scene.use_stock_colors;
vtscControllingCurve = scene.vtsc_controlling_curve;
vtscEnabled = scene.vtsc_enabled;
vtscSpeed = vtscEnabled ? scene.vtsc_speed * speedConversion : setSpeed;
}
void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &painter) {
if (cemStatus && !mapOpen && !hideBottomIcons) {
drawCEMStatus(painter);
}
if (roadNameUI && !bigMapOpen) {
drawRoadName(painter);
}
if (turnSignalAnimation && (turnSignalLeft || turnSignalRight) && !bigMapOpen && ((!mapOpen && standstillDuration == 0) || signalStyle != "static")) {
if (!animationTimer->isActive()) {
animationTimer->start(signalAnimationLength);
}
drawTurnSignals(painter);
} else if (animationTimer->isActive()) {
animationTimer->stop();
}
}
void AnnotatedCameraWidget::drawCEMStatus(QPainter &p) {
if (dmIconPosition == QPoint(0, 0)) {
return;
}
p.save();
p.setOpacity(1.0);
QRect cemWidget(dmIconPosition.x() + (rightHandDM ? -img_size : img_size), dmIconPosition.y() - img_size / 2, img_size, img_size);
if (conditionalStatus == 1 || conditionalStatus == 3 || conditionalStatus == 5) {
p.setPen(QPen(QColor(bg_colors[STATUS_CONDITIONAL_OVERRIDDEN]), 10));
} else if (experimentalMode) {
p.setPen(QPen(QColor(bg_colors[STATUS_EXPERIMENTAL_MODE_ACTIVE]), 10));
} else {
p.setPen(QPen(blackColor(), 10));
}
p.setBrush(blackColor(166));
p.drawRoundedRect(cemWidget, 24, 24);
QSize iconSize(cemWidget.size().width() - 10, cemWidget.size().height() - 10);
QPixmap iconToDraw;
if (conditionalStatus == 1 || conditionalStatus == 3 || conditionalStatus == 5) {
iconToDraw = chillModeIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else if (conditionalStatus == 2 || conditionalStatus == 4 || conditionalStatus == 6) {
iconToDraw = experimentalModeIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else if (conditionalStatus == 7 || conditionalStatus == 8) {
iconToDraw = speedIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else if (conditionalStatus == 9 || conditionalStatus == 11) {
iconToDraw = turnIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else if (conditionalStatus == 10 || conditionalStatus == 15 || conditionalStatus == 16) {
iconToDraw = lightIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else if (conditionalStatus == 12) {
iconToDraw = curveIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else if (conditionalStatus == 13 || conditionalStatus == 14) {
iconToDraw = leadIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
iconToDraw = chillModeIcon.scaled(iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
p.drawPixmap(QRect(cemWidget.center() - QPoint(iconToDraw.width() / 2, iconToDraw.height() / 2), iconToDraw.size()), iconToDraw);
p.restore();
}
PedalIcons::PedalIcons(QWidget *parent) : QWidget(parent) {
setFixedSize(btn_size, btn_size);
brake_pedal_img = loadPixmap("../frogpilot/assets/other_images/brake_pedal.png", {img_size, img_size});
gas_pedal_img = loadPixmap("../frogpilot/assets/other_images/gas_pedal.png", {img_size, img_size});
}
void PedalIcons::updateState(const UIScene &scene) {
acceleration = scene.acceleration;
brakeLightOn = scene.brake_lights_on;
dynamicPedals = scene.dynamic_pedals_on_ui;
standstill = scene.standstill;
staticPedals = scene.static_pedals_on_ui;
accelerating = acceleration > 0.25f;
decelerating = acceleration < -0.25f;
}
void PedalIcons::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
int totalWidth = 2 * img_size;
int startX = (width() - totalWidth) / 2;
int brakeX = startX + img_size / 2;
int gasX = startX + img_size;
float brakeOpacity = 1.0f;
float gasOpacity = 1.0f;
if (dynamicPedals) {
brakeOpacity = standstill ? 1.0f : decelerating ? std::max(0.25f, std::abs(acceleration)) : 0.25f;
gasOpacity = accelerating ? std::max(0.25f, acceleration) : 0.25f;
} else if (staticPedals) {
brakeOpacity = standstill || brakeLightOn || acceleration < -0.5f ? 1.0f : 0.25f;
gasOpacity = !standstill && acceleration > 0 ? 1.0f : 0.25f;
}
p.setOpacity(brakeOpacity);
p.drawPixmap(brakeX, (height() - img_size) / 2, brake_pedal_img);
p.setOpacity(gasOpacity);
p.drawPixmap(gasX, (height() - img_size) / 2, gas_pedal_img);
}
void AnnotatedCameraWidget::drawRoadName(QPainter &p) {
QString roadName = QString::fromStdString(params_memory.get("RoadName"));
if (roadName.isEmpty()) {
return;
}
QFont font = InterFont(40, QFont::DemiBold);
int textWidth = QFontMetrics(font).horizontalAdvance(roadName);
p.save();
QRect roadNameRect((width() - textWidth * 1.25) / 2, rect().bottom() - 55 + 1, textWidth * 1.25, 50);
p.setBrush(blackColor(166));
p.setOpacity(1.0);
p.setPen(QPen(blackColor(), 10));
p.drawRoundedRect(roadNameRect, 24, 24);
p.setFont(font);
p.setPen(QPen(Qt::white, 6));
p.drawText(roadNameRect, Qt::AlignCenter, roadName);
p.restore();
}
void AnnotatedCameraWidget::drawTurnSignals(QPainter &p) {
bool blindspotActive = turnSignalLeft ? blindSpotLeft : blindSpotRight;
if (signalStyle == "static") {
int signalXPosition = turnSignalLeft ? (rect().center().x() * 0.75) - signalWidth : rect().center().x() * 1.25;
int signalYPosition = signalHeight / 2;
if (blindspotActive && !blindspotImages.empty()) {
p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, blindspotImages[turnSignalLeft ? 0 : 1]);
} else {
p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, signalImages[2 * animationFrameIndex + (turnSignalLeft ? 0 : 1)]);
}
} else if (signalStyle == "traditional") {
int signalXPosition = turnSignalLeft ? width() - ((animationFrameIndex + 1) * signalWidth) : animationFrameIndex * signalWidth;
int signalYPosition = height() - signalHeight;
signalYPosition -= alertHeight;
if (blindspotActive && !blindspotImages.empty()) {
p.drawPixmap(turnSignalLeft ? width() - signalWidth : 0, signalYPosition, signalWidth, signalHeight, blindspotImages[turnSignalLeft ? 0 : 1]);
} else {
p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, signalImages[2 * animationFrameIndex + (turnSignalLeft ? 0 : 1)]);
}
} else if (signalStyle == "traditional_gif") {
int signalXPosition = turnSignalLeft ? width() - (animationFrameIndex * signalMovement) + signalWidth : (animationFrameIndex * signalMovement) - signalWidth;
int signalYPosition = height() - signalHeight;
signalYPosition -= alertHeight;
if (blindspotActive && !blindspotImages.empty()) {
p.drawPixmap(turnSignalLeft ? width() - signalWidth : 0, signalYPosition, signalWidth, signalHeight, blindspotImages[turnSignalLeft ? 0 : 1]);
} else {
p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, signalImages[2 * animationFrameIndex + (turnSignalLeft ? 0 : 1)]);
}
}
}