#include #include "frogpilot/ui/qt/onroad/frogpilot_annotated_camera.h" FrogPilotAnnotatedCameraWidget::FrogPilotAnnotatedCameraWidget(QWidget *parent) : QWidget(parent) { animationTimer = new QTimer(this); brakePedalImg = loadPixmap("../../frogpilot/assets/other_images/brake_pedal.png", {btn_size, btn_size}); curveSpeedIcon = loadPixmap("../../frogpilot/assets/other_images/curve_speed.png", {btn_size, btn_size}); dashboardIcon = loadPixmap("../../frogpilot/assets/other_images/dashboard_icon.png", {btn_size / 2, btn_size / 2}); gasPedalImg = loadPixmap("../../frogpilot/assets/other_images/gas_pedal.png", {btn_size, btn_size}); mapDataIcon = loadPixmap("../../frogpilot/assets/other_images/offline_maps_icon.png", {btn_size / 2, btn_size / 2}); navigationIcon = loadPixmap("../../frogpilot/assets/other_images/navigation_icon.png", {btn_size / 2, btn_size / 2}); nextMapsIcon = loadPixmap("../../frogpilot/assets/other_images/next_maps_icon.png", {btn_size / 2, btn_size / 2}); pausedIcon = loadPixmap("../../frogpilot/assets/other_images/paused_icon.png", {btn_size / 2, btn_size / 2}); speedIcon = loadPixmap("../../frogpilot/assets/other_images/speed_icon.png", {btn_size / 2, btn_size / 2}); stopSignImg = loadPixmap("../../frogpilot/assets/other_images/stop_sign.png", {btn_size, btn_size}); turnIcon = loadPixmap("../../frogpilot/assets/other_images/turn_icon.png", {btn_size / 2, btn_size / 2}); loadGif("../../frogpilot/assets/other_images/curve_icon.gif", cemCurveIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/lead_icon.gif", cemLeadIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/speed_icon.gif", cemSpeedIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/light_icon.gif", cemStopIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/turn_icon.gif", cemTurnIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/chill_mode_icon.gif", chillModeIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/experimental_mode_icon.gif", experimentalModeIcon, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/weather_clear_day.gif", weatherClearDay, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/weather_clear_night.gif", weatherClearNight, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/weather_rain.gif", weatherRain, QSize(btn_size / 2, btn_size / 2), this); loadGif("../../frogpilot/assets/other_images/weather_snow.gif", weatherSnow, QSize(btn_size / 2, btn_size / 2), this); QObject::connect(animationTimer, &QTimer::timeout, [this] { animationFrameIndex = (animationFrameIndex + 1) % totalFrames; }); QObject::connect(frogpilotUIState(), &FrogPilotUIState::themeUpdated, this, &FrogPilotAnnotatedCameraWidget::updateSignals); QObject::connect(uiState(), &UIState::offroadTransition, [this] { standstillTimer.invalidate(); QJsonObject stats = QJsonDocument::fromJson(QString::fromStdString(params.get("FrogPilotStats")).toUtf8()).object(); stats["FrogHops"] = stats.value("FrogHops").toInt(0) + frogHopCount; params.putNonBlocking("FrogPilotStats", QJsonDocument(stats).toJson(QJsonDocument::Compact).toStdString()); frogHopCount = 0; }); } void FrogPilotAnnotatedCameraWidget::showEvent(QShowEvent *event) { FrogPilotUIState &fs = *frogpilotUIState(); QJsonObject &frogpilot_toggles = fs.frogpilot_toggles; UIState &s = *uiState(); UIScene &scene = s.scene; if (scene.is_metric || frogpilot_toggles.value("use_si_metrics").toBool()) { accelerationUnit = tr(" m/s²"); leadDistanceUnit = tr(" meters"); leadSpeedUnit = frogpilot_toggles.value("use_si_metrics").toBool() ? tr(" m/s") : tr(" km/h"); distanceConversion = 1.0f; speedConversion = scene.is_metric ? MS_TO_KPH : MS_TO_MPH; speedConversionMetrics = frogpilot_toggles.value("use_si_metrics").toBool() ? 1.0f : MS_TO_KPH; } else { accelerationUnit = tr(" ft/s²"); leadDistanceUnit = tr(" feet"); leadSpeedUnit = tr(" mph"); distanceConversion = METER_TO_FOOT; speedConversion = MS_TO_MPH; speedConversionMetrics = MS_TO_MPH; } updateSignals(); } void FrogPilotAnnotatedCameraWidget::updateSignals() { QVector().swap(blindspotImages); QVector().swap(signalImages); bool isGif = false; QFileInfoList files = QDir("../../frogpilot/assets/active_theme/signals/").entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name); for (const QFileInfo &fileInfo : files) { QString fileName = fileInfo.fileName(); QString filePath = fileInfo.absoluteFilePath(); if (fileName.endsWith(".gif", Qt::CaseInsensitive)) { isGif = true; QMovie movie(filePath); movie.setCacheMode(QMovie::CacheNone); movie.start(); int frameCount = movie.frameCount(); signalImages.reserve(frameCount); for (int i = 0; i < frameCount; ++i) { movie.jumpToFrame(i); QImage image = movie.currentPixmap().toImage().convertToFormat(QImage::Format_Indexed8); QPixmap frame = QPixmap::fromImage(image); signalImages.append(frame); } movie.stop(); } else if (fileName.endsWith(".png", Qt::CaseInsensitive)) { QVector &targetList = fileName.contains("blindspot", Qt::CaseInsensitive) ? blindspotImages : signalImages; targetList.append(QPixmap::fromImage(QImage(filePath).convertToFormat(QImage::Format_Indexed8))); } else { QStringList parts = fileName.split('_'); if (parts.size() == 2) { signalStyle = parts[0]; signalAnimationLength = parts[1].toInt(); } } } if (!signalImages.isEmpty()) { QPixmap &firstImage = signalImages.front(); signalHeight = firstImage.height(); signalWidth = firstImage.width(); totalFrames = signalImages.size(); if (isGif && signalStyle == "traditional") { signalMovement = (width() + signalWidth * 2) / totalFrames; signalStyle = "traditional_gif"; } else { signalMovement = 0; } } else { signalAnimationLength = 0; signalHeight = 0; signalMovement = 0; signalWidth = 0; totalFrames = 0; signalStyle = "None"; } } void FrogPilotAnnotatedCameraWidget::updateState(const FrogPilotUIState &fs, const QJsonObject &frogpilot_toggles) { const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; const SubMaster &fpsm = *(fs.sm); const cereal::FrogPilotPlan::Reader &frogpilotPlan = fpsm["frogpilotPlan"].getFrogpilotPlan(); float speedLimitOffset = frogpilotPlan.getSlcSpeedLimitOffset() * speedConversion; cscSpeedStr = QString::number(std::nearbyint(fmin(speed, frogpilotPlan.getCscSpeed() * speedConversion))) + speedUnit; speedLimitOffsetStr = (speedLimitOffset != 0) ? QString::number(speedLimitOffset, 'f', 0).prepend((speedLimitOffset > 0) ? "+" : "-") : "–"; if (frogpilot_scene.standstill && frogpilot_toggles.value("stopped_timer").toBool()) { if (!standstillTimer.isValid()) { standstillTimer.start(); } else { standstillDuration = frogpilot_scene.map_open || frogpilot_scene.started_timer / UI_FREQ < 60 ? 0 : standstillTimer.elapsed() / 1000; } } else { standstillDuration = 0; standstillTimer.invalidate(); } static int lastFrameIndex; if (lastFrameIndex > animationFrameIndex && frogpilot_toggles.value("signal_icons").toString() == "frog") { frogHopCount++; } lastFrameIndex = animationFrameIndex; update(); } void FrogPilotAnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p, UIState &s, FrogPilotUIState &fs, SubMaster &sm, SubMaster &fpsm, QJsonObject &frogpilot_toggles) { FrogPilotUIScene &frogpilot_scene = fs.frogpilot_scene; UIScene &scene = s.scene; const cereal::CarState::Reader &carState = fpsm["carState"].getCarState(); const cereal::FrogPilotCarState::Reader &frogpilotCarState = fpsm["frogpilotCarState"].getFrogpilotCarState(); const cereal::FrogPilotNavigation::Reader &frogpilotNavigation = fpsm["frogpilotNavigation"].getFrogpilotNavigation(); const cereal::FrogPilotPlan::Reader &frogpilotPlan = fpsm["frogpilotPlan"].getFrogpilotPlan(); const cereal::ModelDataV2::Reader &model = sm["modelV2"].getModelV2(); if (!hideBottomIcons && frogpilot_toggles.value("cem_status").toBool()) { paintCEMStatus(p, frogpilotPlan, frogpilot_scene, sm); } else { cemStatusPosition.setX(0); cemStatusPosition.setY(0); } if (!frogpilot_scene.map_open && !hideBottomIcons && frogpilot_toggles.value("compass").toBool()) { paintCompass(p, frogpilot_toggles); } else { compassPosition.setX(0); compassPosition.setY(0); } if (!frogpilot_scene.map_open && !frogpilotPlan.getSpeedLimitChanged() && !(signalStyle == "static" && carState.getLeftBlinker()) && frogpilot_toggles.value("csc_status").toBool()) { if (frogpilotPlan.getCscTraining()) { paintSmartControllerTraining(p, frogpilotPlan); } else { glowTimer.invalidate(); if (isCruiseSet && frogpilotPlan.getCscControllingSpeed()) { paintCurveSpeedControl(p, frogpilotPlan); } } } else { glowTimer.invalidate(); } if (!frogpilot_scene.map_open && frogpilotCarState.getPauseLateral() && !hideBottomIcons) { paintLateralPaused(p, frogpilot_scene); } else { lateralPausedPosition.setX(0); lateralPausedPosition.setY(0); } if (!frogpilot_scene.map_open && (frogpilotCarState.getForceCoast() || frogpilotCarState.getPauseLongitudinal()) && !hideBottomIcons) { paintLongitudinalPaused(p, frogpilot_scene); } if (!bigMapOpen && frogpilot_toggles.value("pedals_on_ui").toBool()) { paintPedalIcons(p, carState, frogpilotCarState, frogpilot_scene, frogpilot_toggles); } if (frogpilotPlan.getSpeedLimitChanged()) { paintPendingSpeedLimit(p, frogpilotPlan); } else { pendingLimitTimer.invalidate(); } if (frogpilot_toggles.value("radar_tracks").toBool()) { paintRadarTracks(p, model, s, frogpilot_scene, sm, fpsm); } if (frogpilot_toggles.value("road_name_ui").toBool()) { paintRoadName(p); } if (!bigMapOpen && (mutcdSpeedLimit || viennaSpeedLimit) && frogpilot_toggles.value("speed_limit_sources").toBool()) { paintSpeedLimitSources(p, frogpilotCarState, frogpilotNavigation, frogpilotPlan); } if (!frogpilot_scene.map_open && standstillDuration != 0 && frogpilot_scene.started_timer / UI_FREQ >= 60) { paintStandstillTimer(p); } if (scene.track_vertices.length() >= 1 && frogpilotPlan.getRedLight() && frogpilot_toggles.value("show_stopping_point").toBool()) { paintStoppingPoint(p, scene, frogpilot_scene, frogpilot_toggles); } if (!bigMapOpen && (carState.getLeftBlinker() || carState.getRightBlinker()) && signalStyle != "None") { if (!animationTimer->isActive()) { animationTimer->start(signalAnimationLength); } paintTurnSignals(p, carState); } else if (animationTimer->isActive()) { animationTimer->stop(); } if (!frogpilot_scene.map_open && !hideBottomIcons) { paintWeather(p, frogpilotPlan, frogpilot_scene); } } void FrogPilotAnnotatedCameraWidget::paintAdjacentPaths(QPainter &p, const cereal::CarState::Reader &carState, const FrogPilotUIScene &frogpilot_scene, const QJsonObject &frogpilot_toggles) { std::function drawAdjacentPath = [&p, &frogpilot_toggles, this](bool isBlindSpot, float width, float requirement, const QPolygonF &polygon) { QLinearGradient gradient(0, height(), 0, 0); if (isBlindSpot && frogpilot_toggles.value("blind_spot_path").toBool()) { gradient.setColorAt(0.0f, QColor::fromHslF(0 / 360.0f, 0.75f, 0.5f, 0.6f)); gradient.setColorAt(0.5f, QColor::fromHslF(0 / 360.0f, 0.75f, 0.5f, 0.4f)); gradient.setColorAt(1.0f, QColor::fromHslF(0 / 360.0f, 0.75f, 0.5f, 0.2f)); } else { float ratio = std::clamp(width / requirement, 0.0f, 1.0f); float hue = ratio * (120.0f / 360.0f); gradient.setColorAt(0.0f, QColor::fromHslF(hue, 0.75f, 0.5f, 0.6f)); gradient.setColorAt(0.5f, QColor::fromHslF(hue, 0.75f, 0.5f, 0.4f)); gradient.setColorAt(1.0f, QColor::fromHslF(hue, 0.75f, 0.5f, 0.2f)); } p.setBrush(gradient); p.drawPolygon(polygon); }; std::function drawAdjacentPathMetric = [&p, &frogpilot_toggles, this](bool isBlindSpot, float width, const QPolygonF &polygon) { QString text = isBlindSpot && frogpilot_toggles.value("blind_spot_path").toBool() ? tr("Vehicle in blind spot") : QString::number(width * distanceConversion, 'f', 2) + leadDistanceUnit; p.setFont(InterFont(40, QFont::DemiBold)); p.setPen(QPen(whiteColor())); p.drawText(polygon.boundingRect(), Qt::AlignCenter, text); }; if (frogpilot_scene.lane_width_left >= frogpilot_toggles.value("lane_detection_width").toDouble()) { p.save(); drawAdjacentPath(carState.getLeftBlindspot(), frogpilot_scene.lane_width_left, frogpilot_toggles.value("lane_detection_width").toDouble(), frogpilot_scene.track_adjacent_vertices[0]); if (frogpilot_toggles.value("adjacent_path_metrics").toBool()) { drawAdjacentPathMetric(carState.getLeftBlindspot(), frogpilot_scene.lane_width_left, frogpilot_scene.track_adjacent_vertices[0]); } p.restore(); } if (frogpilot_scene.lane_width_right >= frogpilot_toggles.value("lane_detection_width").toDouble()) { p.save(); drawAdjacentPath(carState.getRightBlindspot(), frogpilot_scene.lane_width_right, frogpilot_toggles.value("lane_detection_width").toDouble(), frogpilot_scene.track_adjacent_vertices[1]); if (frogpilot_toggles.value("adjacent_path_metrics").toBool()) { drawAdjacentPathMetric(carState.getRightBlindspot(), frogpilot_scene.lane_width_right, frogpilot_scene.track_adjacent_vertices[1]); } p.restore(); } } void FrogPilotAnnotatedCameraWidget::paintBlindSpotPath(QPainter &p, const cereal::CarState::Reader &carState, const FrogPilotUIScene &frogpilot_scene) { p.save(); 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)); p.setBrush(bs); if (frogpilot_scene.lane_width_left != 0 && carState.getLeftBlindspot()) { p.drawPolygon(frogpilot_scene.track_adjacent_vertices[0]); } if (frogpilot_scene.lane_width_right != 0 && carState.getRightBlindspot()) { p.drawPolygon(frogpilot_scene.track_adjacent_vertices[1]); } p.restore(); } void FrogPilotAnnotatedCameraWidget::paintCEMStatus(QPainter &p, const cereal::FrogPilotPlan::Reader &frogpilotPlan, FrogPilotUIScene &frogpilot_scene, SubMaster &sm) { if (dmIconPosition == QPoint(0, 0)) { return; } p.save(); cemStatusPosition.rx() = dmIconPosition.x(); cemStatusPosition.ry() = dmIconPosition.y() - widget_size / 2; cemStatusPosition.rx() += (rightHandDM ? -img_size - widget_size : widget_size) / (frogpilot_scene.map_open ? 1.25 : 1); QRect cemWidget(cemStatusPosition, QSize(widget_size, widget_size)); p.setBrush(blackColor(166)); if (frogpilot_scene.conditional_status == 1) { p.setPen(QPen(QColor(bg_colors[STATUS_CONDITIONAL_OVERRIDDEN]), 10)); } else if (frogpilot_scene.enabled && sm["controlsState"].getControlsState().getExperimentalMode()) { p.setPen(QPen(QColor(bg_colors[STATUS_EXPERIMENTAL_MODE_ENABLED]), 10)); } else { p.setPen(QPen(blackColor(), 10)); } p.drawRoundedRect(cemWidget, 24, 24); QSharedPointer icon = chillModeIcon; if (frogpilot_scene.enabled && sm["controlsState"].getControlsState().getExperimentalMode()) { if (frogpilot_scene.conditional_status == 1) { icon = chillModeIcon; } else if (frogpilot_scene.conditional_status == 2) { icon = experimentalModeIcon; } else if (frogpilot_scene.conditional_status == 3 || frogpilot_scene.conditional_status == 4) { icon = cemSpeedIcon; } else if (frogpilot_scene.conditional_status == 5 || frogpilot_scene.conditional_status == 7) { icon = cemTurnIcon; } else if (frogpilot_scene.conditional_status == 6 || frogpilot_scene.conditional_status == 11 || frogpilot_scene.conditional_status == 12) { icon = cemStopIcon; } else if (frogpilot_scene.conditional_status == 8) { icon = cemCurveIcon; } else if (frogpilot_scene.conditional_status == 9 || frogpilot_scene.conditional_status == 10) { icon = cemLeadIcon; } else { icon = experimentalModeIcon; } } p.drawPixmap(cemWidget, icon->currentPixmap()); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintCompass(QPainter &p, QJsonObject &frogpilot_toggles) { p.save(); compassPosition.rx() = rightHandDM ? UI_BORDER_SIZE + widget_size / 2 : width() - UI_BORDER_SIZE - btn_size; if (mapButtonVisible) { if (rightHandDM) { compassPosition.rx() += btn_size - UI_BORDER_SIZE; } else { compassPosition.rx() -= btn_size + UI_BORDER_SIZE; } } compassPosition.ry() = dmIconPosition.y() - widget_size / 2; QRect compassWidget(compassPosition, QSize(widget_size, widget_size)); p.setBrush(blackColor(166)); p.setPen(QPen(blackColor(), 10)); p.drawRoundedRect(compassWidget, 24, 24); QPainterPath clipPath; clipPath.addRoundedRect(compassWidget.adjusted(5, 5, -5, -5), 24, 24); p.setClipPath(clipPath); QFont font = InterFont(65, QFont::Bold); QFontMetrics fm(font); p.setFont(font); p.setPen(QPen(whiteColor())); const double pixelsPerDegree = 2.5; const int baseRibbonWidth = qRound(360 * pixelsPerDegree); static QPixmap ribbonPixmap; if (!ribbonPixmap) { int ribbonHeight = compassWidget.height(); int ribbonWidth = baseRibbonWidth * 2; ribbonPixmap = QPixmap(ribbonWidth, ribbonHeight); ribbonPixmap.fill(Qt::transparent); QPainter ribbonPainter(&ribbonPixmap); ribbonPainter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); ribbonPainter.setFont(font); QMap directionLabels = {{0, "N"}, {45, "NE"}, {90, "E"}, {135, "SE"}, {180, "S"}, {225, "SW"}, {270, "W"}, {315, "NW"}, {360, "N"}}; for (int i = 0; i < 2; ++i) { for (int degree = 0; degree < 360; ++degree) { int x = qRound((i * 360 + degree) * pixelsPerDegree); if (directionLabels.contains(degree)) { const QString &label = directionLabels[degree]; int textX = x - fm.horizontalAdvance(label) / 2; ribbonPainter.setPen(whiteColor()); ribbonPainter.drawText(textX, fm.ascent(), label); } int notchHeight = (degree % 45 == 0) ? 35 : (degree % 15 == 0) ? 25 : 15; int notchWidth = (degree % 45 == 0) ? 5 : (degree % 15 == 0) ? 4 : 3; ribbonPainter.setPen(QPen(whiteColor(), notchWidth)); ribbonPainter.drawLine(x, ribbonHeight - notchHeight - 5, x, ribbonHeight); } } } double rawBearing = QJsonDocument::fromJson(QString::fromStdString(params_memory.get("LastGPSPosition")).toUtf8()).object().value("bearing").toDouble(0.0); int bearing = qRound(fmod(rawBearing + 360.0, 360.0)); int offset = qRound(bearing * pixelsPerDegree) % baseRibbonWidth; int drawX = compassWidget.center().x() - offset; p.drawPixmap(drawX - baseRibbonWidth, compassWidget.top() + 5, ribbonPixmap); p.drawPixmap(drawX, compassWidget.top() + 5, ribbonPixmap); int triangleSize = 40; int triangleX = compassWidget.center().x(); int triangleY = compassWidget.bottom() - triangleSize; QPolygon triangle(QVector{ QPoint(triangleX, triangleY - triangleSize), QPoint(triangleX - triangleSize / 1.5, triangleY), QPoint(triangleX + triangleSize / 1.5, triangleY) }); p.setBrush(whiteColor()); p.setPen(Qt::NoPen); p.drawPolygon(triangle); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintCurveSpeedControl(QPainter &p, const cereal::FrogPilotPlan::Reader &frogpilotPlan) { p.save(); QRect curveSpeedRect(QPoint(setSpeedRect.right() + UI_BORDER_SIZE, setSpeedRect.top()), QSize(defaultSize.width() * 1.25, defaultSize.width() * 1.25)); QPixmap curveSpeedImage = frogpilotPlan.getRoadCurvature() < 0 ? curveSpeedIcon : curveSpeedIcon.transformed(QTransform().scale(-1, 1)); QSize curveSpeedSize = curveSpeedImage.size(); QPoint curveSpeedPoint(curveSpeedRect.x() + (curveSpeedRect.width() - curveSpeedSize.width()) / 2, curveSpeedRect.y() + (curveSpeedRect.height() - curveSpeedSize.height()) / 2); p.setOpacity(1.0); QRect cscRect(curveSpeedRect.topLeft() + QPoint(0, curveSpeedRect.height() + 10), QSize(curveSpeedRect.width(), 100)); p.setBrush(blueColor(166)); p.setFont(InterFont(45, QFont::Bold)); p.setPen(QPen(blueColor(), 10)); p.drawRoundedRect(cscRect, 24, 24); p.setPen(QPen(whiteColor(), 6)); p.drawText(cscRect.adjusted(20, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, cscSpeedStr); p.drawPixmap(curveSpeedPoint, curveSpeedImage); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintLateralPaused(QPainter &p, FrogPilotUIScene &frogpilot_scene) { if (dmIconPosition == QPoint(0, 0)) { return; } p.save(); if (cemStatusPosition != QPoint(0, 0)) { lateralPausedPosition = cemStatusPosition; } else { lateralPausedPosition.rx() = dmIconPosition.x(); lateralPausedPosition.ry() = dmIconPosition.y() - widget_size / 2; } lateralPausedPosition.rx() += (rightHandDM ? -UI_BORDER_SIZE - widget_size - UI_BORDER_SIZE : UI_BORDER_SIZE + widget_size + UI_BORDER_SIZE) / (frogpilot_scene.map_open ? 1.25 : 1); QRect lateralWidget(lateralPausedPosition, QSize(widget_size, widget_size)); p.setBrush(blackColor(166)); p.setPen(QPen(QColor(bg_colors[STATUS_TRAFFIC_MODE_ENABLED]), 10)); p.drawRoundedRect(lateralWidget, 24, 24); p.setOpacity(0.5); p.drawPixmap(lateralWidget, turnIcon); p.setOpacity(0.75); p.drawPixmap(lateralWidget, pausedIcon); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintLeadMetrics(QPainter &p, bool adjacent, QPointF *chevron, const cereal::FrogPilotPlan::Reader &frogpilotPlan, const cereal::RadarState::LeadData::Reader &lead_data) { float leadDistance = lead_data.getDRel() + (adjacent ? fabs(lead_data.getYRel()) : 0); float leadSpeed = std::max(lead_data.getVLead(), 0.0f); p.setFont(InterFont(40, QFont::Bold)); p.setPen(QPen(whiteColor())); QString text; if (adjacent) { text = QString("%1 %2 | %3 %4") .arg(qRound(leadDistance * distanceConversion)) .arg(leadDistanceUnit) .arg(qRound(leadSpeed * speedConversionMetrics)) .arg(leadSpeedUnit); } else { text = QString("%1 %2 (%3) | %4 %5 | %6 %7") .arg(qRound(leadDistance * distanceConversion)) .arg(leadDistanceUnit) .arg(QString(tr("Desired: %1")).arg(frogpilotPlan.getDesiredFollowDistance() * distanceConversion)) .arg(qRound(leadSpeed * speedConversionMetrics)) .arg(leadSpeedUnit) .arg(QString::number(leadDistance / std::max(speed / speedConversion, 1.0f), 'f', 2)) .arg(tr("s")); } QFontMetrics metrics(p.font()); int textHeight = metrics.height(); int textWidth = metrics.horizontalAdvance(text); int textX = ((chevron[2].x() + chevron[0].x()) / 2) - textWidth / 2; int textY = chevron[0].y() + textHeight + 5; if (!adjacent) { int xMargin = textWidth * 0.25; int yMargin = textHeight * 0.25; leadTextRect = QRect(textX, textY - textHeight, textWidth, textHeight).adjusted(-xMargin, -yMargin, xMargin, yMargin); p.drawText(textX, textY, text); } else { QRect adjacentTextRect(textX, textY - textHeight, textWidth, textHeight); if (!adjacentTextRect.intersects(leadTextRect)) { p.drawText(textX, textY, text); } } } void FrogPilotAnnotatedCameraWidget::paintLongitudinalPaused(QPainter &p, FrogPilotUIScene &frogpilot_scene) { if (dmIconPosition == QPoint(0, 0)) { return; } p.save(); QPoint longitudinalIconPosition; if (lateralPausedPosition != QPoint(0, 0)) { longitudinalIconPosition = lateralPausedPosition; } else if (cemStatusPosition != QPoint(0, 0)) { longitudinalIconPosition = cemStatusPosition; } else { longitudinalIconPosition.rx() = dmIconPosition.x(); longitudinalIconPosition.ry() = dmIconPosition.y() - widget_size / 2; } longitudinalIconPosition.rx() += (rightHandDM ? -UI_BORDER_SIZE - widget_size - UI_BORDER_SIZE : UI_BORDER_SIZE + widget_size + UI_BORDER_SIZE) / (frogpilot_scene.map_open ? 1.25 : 1); QRect longitudinalWidget(longitudinalIconPosition, QSize(widget_size, widget_size)); p.setBrush(blackColor(166)); p.setPen(QPen(QColor(bg_colors[STATUS_TRAFFIC_MODE_ENABLED]), 10)); p.drawRoundedRect(longitudinalWidget, 24, 24); p.setOpacity(0.5); p.drawPixmap(longitudinalWidget, speedIcon); p.setOpacity(0.75); p.drawPixmap(longitudinalWidget, pausedIcon); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintPathEdges(QPainter &p, const cereal::NavInstruction::Reader &navInstruction, const UIScene &scene, const FrogPilotUIScene &frogpilot_scene, SubMaster &sm) { p.save(); std::function setPathEdgeColors = [&](QLinearGradient &gradient, QColor baseColor) { baseColor.setAlphaF(1.0f); gradient.setColorAt(0.0f, baseColor); baseColor.setAlphaF(0.5f); gradient.setColorAt(0.5f, baseColor); baseColor.setAlphaF(0.1f); gradient.setColorAt(1.0f, baseColor); }; QLinearGradient pe(0, height(), 0, 0); if (frogpilot_scene.always_on_lateral_active) { setPathEdgeColors(pe, bg_colors[STATUS_ALWAYS_ON_LATERAL_ACTIVE]); } else if (frogpilot_scene.conditional_status == 1) { setPathEdgeColors(pe, bg_colors[STATUS_CONDITIONAL_OVERRIDDEN]); } else if (sm["controlsState"].getControlsState().getExperimentalMode()) { setPathEdgeColors(pe, bg_colors[STATUS_EXPERIMENTAL_MODE_ENABLED]); } else if (frogpilot_scene.traffic_mode_enabled) { setPathEdgeColors(pe, bg_colors[STATUS_TRAFFIC_MODE_ENABLED]); } else if (frogpilot_scene.model_length > navInstruction.getManeuverDistance() && navInstruction.getManeuverDistance() >= 1) { setPathEdgeColors(pe, bg_colors[STATUS_NAVIGATION_ACTIVE]); } else if (!frogpilot_scene.use_stock_colors) { setPathEdgeColors(pe, frogpilot_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(frogpilot_scene.track_edge_vertices); p.setBrush(pe); p.drawPath(path); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintPedalIcons(QPainter &p, const cereal::CarState::Reader &carState, const cereal::FrogPilotCarState::Reader &frogpilotCarState, FrogPilotUIScene &frogpilot_scene, QJsonObject &frogpilot_toggles) { p.save(); float brakeOpacity = 1.0f; float gasOpacity = 1.0f; if (frogpilot_toggles.value("dynamic_pedals_on_ui").toBool()) { brakeOpacity = frogpilot_scene.standstill ? 1.0f : carState.getAEgo() < -0.25f ? std::max(0.25f, std::abs(carState.getAEgo())) : 0.25f; gasOpacity = std::max(0.25f, carState.getAEgo()); } else if (frogpilot_toggles.value("static_pedals_on_ui").toBool()) { brakeOpacity = frogpilot_scene.standstill || frogpilotCarState.getBrakeLights() || carState.getAEgo() < -0.25f ? 1.0f : 0.25f; gasOpacity = carState.getAEgo() > 0.25 ? 1.0f : 0.25f; } int startX = experimentalButtonPosition.x(); int startY = experimentalButtonPosition.y() + btn_size + UI_BORDER_SIZE; p.setOpacity(brakeOpacity); p.drawPixmap(startX, startY, brakePedalImg); p.setOpacity(gasOpacity); p.drawPixmap(startX + btn_size / 2, startY, gasPedalImg); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintPendingSpeedLimit(QPainter &p, const cereal::FrogPilotPlan::Reader &frogpilotPlan) { p.save(); if (!pendingLimitTimer.isValid()) { pendingLimitTimer.start(); } QString newSpeedLimitStr = (frogpilotPlan.getUnconfirmedSlcSpeedLimit() > 1) ? QString::number(std::nearbyint(frogpilotPlan.getUnconfirmedSlcSpeedLimit() * speedConversion)) : "–"; newSpeedLimitRect = speedLimitRect.translated(speedLimitRect.width() + UI_BORDER_SIZE, 0); if (!viennaSpeedLimit) { newSpeedLimitRect.setWidth(newSpeedLimitStr.size() >= 3 ? 200 : 175); p.setBrush(whiteColor()); p.setPen(Qt::NoPen); p.drawRoundedRect(newSpeedLimitRect, 24, 24); p.setPen(pendingLimitTimer.elapsed() % 1000 < 500 ? QPen(blackColor(), 6) : QPen(redColor(), 6)); p.drawRoundedRect(newSpeedLimitRect.adjusted(9, 9, -9, -9), 16, 16); p.setFont(InterFont(28, QFont::DemiBold)); p.drawText(newSpeedLimitRect.adjusted(0, 22, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("PENDING")); p.drawText(newSpeedLimitRect.adjusted(0, 51, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT")); p.setFont(InterFont(70, QFont::Bold)); p.drawText(newSpeedLimitRect.adjusted(0, 85, 0, 0), Qt::AlignTop | Qt::AlignHCenter, newSpeedLimitStr); } else { p.setBrush(whiteColor()); p.setPen(Qt::NoPen); p.drawEllipse(newSpeedLimitRect); p.setPen(QPen(Qt::red, 20)); p.drawEllipse(newSpeedLimitRect.adjusted(16, 16, -16, -16)); p.setPen(pendingLimitTimer.elapsed() % 1000 < 500 ? QPen(blackColor(), 6) : QPen(redColor(), 6)); p.setFont(InterFont((newSpeedLimitStr.size() >= 3) ? 60 : 70, QFont::Bold)); p.drawText(newSpeedLimitRect, Qt::AlignCenter, newSpeedLimitStr); } p.restore(); } void FrogPilotAnnotatedCameraWidget::paintRadarTracks(QPainter &p, const cereal::ModelDataV2::Reader &model, UIState &s, FrogPilotUIScene &frogpilot_scene, SubMaster &sm, SubMaster &fpsm) { p.save(); capnp::List::Reader liveTracks = fpsm["liveTracks"].getLiveTracks(); update_radar_tracks(liveTracks, model.getPosition(), s, sm); int diameter = 25; QRect viewport = p.viewport(); for (std::size_t i = 0; i < frogpilot_scene.live_radar_tracks.size(); ++i) { const RadarTrackData &track = frogpilot_scene.live_radar_tracks[i]; float x = std::clamp(static_cast(track.calibrated_point.x()), 0.0f, float(viewport.width() - diameter)); float y = std::clamp(static_cast(track.calibrated_point.y()), 0.0f, float(viewport.height() - diameter)); p.setBrush(redColor()); p.drawEllipse(QPointF(x + diameter / 2.0f, y + diameter / 2.0f), diameter / 2.0f, diameter / 2.0f); } p.restore(); } void FrogPilotAnnotatedCameraWidget::paintRainbowPath(QPainter &p, QLinearGradient &bg, float lin_grad_point, SubMaster &sm) { p.save(); static float hueOffset = 0.0; if (sm["carState"].getCarState().getVEgo() > 0) { hueOffset += powf(sm["carState"].getCarState().getVEgo(), 0.5f) / sqrtf(145.0f / MS_TO_KPH); } float alpha = util::map_val(lin_grad_point, 0.0f, 1.0f, 0.5f, 0.1f); float pathHue = fmodf((lin_grad_point * 360.0f) + hueOffset, 360.0f); bg.setColorAt(lin_grad_point, QColor::fromHslF(pathHue / 360.0f, 1.0f, 0.5f, alpha)); bg.setSpread(QGradient::RepeatSpread); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintRoadName(QPainter &p) { QString roadName = QString::fromStdString(params_memory.get("RoadName")); if (roadName.isEmpty()) { return; } alertHeight = std::max(50, alertHeight); p.save(); QFont font = InterFont(40, QFont::DemiBold); int textWidth = QFontMetrics(font).horizontalAdvance(roadName); QRect roadNameRect((width() - (textWidth + 100)) / 2, rect().bottom() - 55 + 1, textWidth + 100, 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(whiteColor(), 6)); p.drawText(roadNameRect, Qt::AlignCenter, roadName); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintSmartControllerTraining(QPainter &p, const cereal::FrogPilotPlan::Reader &frogpilotPlan) { p.save(); if (!glowTimer.isValid()) { glowTimer.start(); } QRect curveSpeedRect(QPoint(setSpeedRect.right() + UI_BORDER_SIZE, setSpeedRect.top()), QSize(defaultSize.width() * 1.25, defaultSize.width() * 1.25)); QPixmap curveSpeedImage = frogpilotPlan.getRoadCurvature() < 0 ? curveSpeedIcon : curveSpeedIcon.transformed(QTransform().scale(-1, 1)); qreal phase = (glowTimer.elapsed() % 2000) / 2000.0 * 2 * M_PI; qreal alphaFactor = 0.5 + 0.5 * sin(phase); QColor glowColor = blueColor(); glowColor.setAlphaF(0.3 + 0.7 * alphaFactor); int glowWidth = 8 + static_cast(2 * alphaFactor); p.setOpacity(1.0); p.setBrush(blackColor(166)); p.setPen(QPen(glowColor, glowWidth)); p.drawRoundedRect(curveSpeedRect, 24, 24); QSize curveSpeedSize = curveSpeedImage.size(); QPoint curveSpeedPoint(curveSpeedRect.x() + (curveSpeedRect.width() - curveSpeedSize.width()) / 2, curveSpeedRect.y() + (curveSpeedRect.height() - curveSpeedSize.height()) / 2); p.drawPixmap(curveSpeedPoint, curveSpeedImage); QRect textRect(curveSpeedRect.topLeft() + QPoint(0, curveSpeedRect.height() + 10), QSize(curveSpeedRect.width(), 50)); p.setBrush(blackColor(166)); p.setPen(QPen(blackColor(), 10)); p.drawRoundedRect(textRect, 24, 24); p.setFont(InterFont(35, QFont::Bold)); p.setPen(QPen(whiteColor(), 6)); p.drawText(textRect.adjusted(20, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, "Training..."); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintSpeedLimitSources(QPainter &p, const cereal::FrogPilotCarState::Reader &frogpilotCarState, const cereal::FrogPilotNavigation::Reader &frogpilotNavigation, const cereal::FrogPilotPlan::Reader &frogpilotPlan) { p.save(); std::function drawSource = [&](QRect &rect, QPixmap &icon, QString title, double speedLimitValue) { if (QString::fromUtf8(frogpilotPlan.getSlcSpeedLimitSource().cStr()) == "Mapbox" && title == "Navigation") { speedLimitValue = frogpilotPlan.getSlcMapboxSpeedLimit() * speedConversion; title = "Mapbox"; } if (QString::fromUtf8(frogpilotPlan.getSlcSpeedLimitSource().cStr()) == title && speedLimitValue != 0) { p.setBrush(redColor(166)); p.setFont(InterFont(35, QFont::Bold)); p.setPen(QPen(redColor(), 10)); } else { p.setBrush(blackColor(166)); p.setFont(InterFont(35, QFont::DemiBold)); p.setPen(QPen(blackColor(), 10)); } 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 != 0) { 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(speedLimitRect.x() - signMargin, speedLimitRect.y() + speedLimitRect.height() + UI_BORDER_SIZE, 450, 60); QRect mapDataRect(dashboardRect.x(), dashboardRect.y() + dashboardRect.height() + UI_BORDER_SIZE / 2, 450, 60); QRect navigationRect(mapDataRect.x(), mapDataRect.y() + mapDataRect.height() + UI_BORDER_SIZE / 2, 450, 60); QRect nextLimitRect(navigationRect.x(), navigationRect.y() + navigationRect.height() + UI_BORDER_SIZE / 2, 450, 60); drawSource(dashboardRect, dashboardIcon, "Dashboard", frogpilotCarState.getDashboardSpeedLimit() * speedConversion); drawSource(mapDataRect, mapDataIcon, "Map Data", frogpilotPlan.getSlcMapSpeedLimit() * speedConversion); drawSource(navigationRect, navigationIcon, "Navigation", frogpilotNavigation.getNavigationSpeedLimit() * speedConversion); drawSource(nextLimitRect, nextMapsIcon, "Upcoming", frogpilotPlan.getSlcNextSpeedLimit() * speedConversion); p.restore(); } void FrogPilotAnnotatedCameraWidget::paintStandstillTimer(QPainter &p) { p.save(); float transition = 0.0f; QColor startColor, endColor; if (standstillDuration < 60) { startColor = endColor = bg_colors[STATUS_ENGAGED]; } else if (standstillDuration < 150) { startColor = bg_colors[STATUS_ENGAGED]; endColor = bg_colors[STATUS_CONDITIONAL_OVERRIDDEN]; transition = (standstillDuration - 60) / 150.0f; } else if (standstillDuration < 300) { startColor = bg_colors[STATUS_CONDITIONAL_OVERRIDDEN]; endColor = bg_colors[STATUS_TRAFFIC_MODE_ENABLED]; transition = (standstillDuration - 150) / 150.0f; } else { startColor = endColor = bg_colors[STATUS_TRAFFIC_MODE_ENABLED]; transition = 0.0f; } QColor blendedColor( startColor.red() + transition * (endColor.red() - startColor.red()), startColor.green() + transition * (endColor.green() - startColor.green()), startColor.blue() + transition * (endColor.blue() - startColor.blue()) ); int minutes = standstillDuration / 60; int seconds = standstillDuration % 60; p.setFont(InterFont(176, QFont::Bold)); { QString minuteStr = (minutes == 1) ? tr("1 minute") : QString(tr("%1 minutes")).arg(minutes); QRect textRect = p.fontMetrics().boundingRect(minuteStr); textRect.moveCenter({rect().center().x(), 210 - textRect.height() / 2}); p.setPen(QPen(blendedColor)); p.drawText(textRect.x(), textRect.bottom(), minuteStr); } p.setFont(InterFont(66)); { QString secondStr = (seconds == 1) ? tr("1 second") : QString(tr("%1 seconds")).arg(seconds); QRect textRect = p.fontMetrics().boundingRect(secondStr); textRect.moveCenter({rect().center().x(), 290 - textRect.height() / 2}); p.setPen(QPen(whiteColor())); p.drawText(textRect.x(), textRect.bottom(), secondStr); } p.restore(); } void FrogPilotAnnotatedCameraWidget::paintStoppingPoint(QPainter &p, UIScene &scene, FrogPilotUIScene &frogpilot_scene, QJsonObject &frogpilot_toggles) { p.save(); QPointF centerPoint = (scene.track_vertices.first() + scene.track_vertices.last()) / 2.0; QPointF adjustedPoint = centerPoint - QPointF(stopSignImg.width() / 2, stopSignImg.height()); p.drawPixmap(adjustedPoint, stopSignImg); if (frogpilot_toggles.value("show_stopping_point_metrics").toBool()) { QFont font = InterFont(35, QFont::DemiBold); QString text = QString::number(std::nearbyint(frogpilot_scene.model_length * distanceConversion)) + leadDistanceUnit; QPointF textPosition = centerPoint - QPointF(QFontMetrics(font).horizontalAdvance(text) / 2, stopSignImg.height() + 35); p.setFont(font); p.setPen(QPen(whiteColor())); p.drawText(textPosition, text); } p.restore(); } void FrogPilotAnnotatedCameraWidget::paintTurnSignals(QPainter &p, const cereal::CarState::Reader &carState) { p.save(); bool leftBlinker = carState.getLeftBlinker(); bool blindspotActive = leftBlinker ? carState.getLeftBlindspot() : carState.getRightBlindspot(); if (signalStyle == "static") { int signalXPosition = leftBlinker ? (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[0].transformed(QTransform().scale(leftBlinker ? 1 : -1, 1))); } else { p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, signalImages[animationFrameIndex].transformed(QTransform().scale(leftBlinker ? 1 : -1, 1))); } } else { int signalXPosition; if (signalStyle == "traditional_gif") { signalXPosition = leftBlinker ? width() - (animationFrameIndex * signalMovement) + signalWidth : (animationFrameIndex * signalMovement) - signalWidth; } else { signalXPosition = leftBlinker ? width() - ((animationFrameIndex + 1) * signalWidth) : animationFrameIndex * signalWidth; } int signalYPosition = height() - signalHeight - alertHeight; if (blindspotActive && !blindspotImages.empty()) { p.drawPixmap(leftBlinker ? width() - signalWidth : 0, signalYPosition, signalWidth, signalHeight, blindspotImages[0].transformed(QTransform().scale(leftBlinker ? 1 : -1, 1))); } else { p.drawPixmap(signalXPosition, signalYPosition, signalWidth, signalHeight, signalImages[animationFrameIndex].transformed(QTransform().scale(leftBlinker ? 1 : -1, 1))); } } p.restore(); } void FrogPilotAnnotatedCameraWidget::paintWeather(QPainter &p, const cereal::FrogPilotPlan::Reader &frogpilotPlan, FrogPilotUIScene &frogpilot_scene) { int weatherId = frogpilotPlan.getWeatherId(); if (weatherId == 0) { return; } p.save(); QPoint weatherIconPosition; if (compassPosition != QPoint(0, 0)) { weatherIconPosition = compassPosition; weatherIconPosition.rx() += (rightHandDM ? UI_BORDER_SIZE + widget_size + UI_BORDER_SIZE : -UI_BORDER_SIZE - widget_size - UI_BORDER_SIZE) / (frogpilot_scene.map_open ? 1.25 : 1); } else { weatherIconPosition.rx() = rightHandDM ? UI_BORDER_SIZE + widget_size / 2 : width() - UI_BORDER_SIZE - btn_size; if (mapButtonVisible) { if (rightHandDM) { weatherIconPosition.rx() += btn_size - UI_BORDER_SIZE; } else { weatherIconPosition.rx() -= btn_size + UI_BORDER_SIZE; } } weatherIconPosition.ry() = dmIconPosition.y() - widget_size / 2; } QRect weatherRect(weatherIconPosition, QSize(widget_size, widget_size)); p.setBrush(blackColor(166)); p.setPen(QPen(blackColor(), 10)); p.drawRoundedRect(weatherRect, 24, 24); QSharedPointer icon = weatherClearDay; if ((weatherId >= 200 && weatherId <= 232) || (weatherId >= 300 && weatherId <= 321) || (weatherId >= 500 && weatherId <= 531)) { icon = weatherRain; } else if (weatherId >= 600 && weatherId <= 622) { icon = weatherSnow; } else if (weatherId == 800) { icon = frogpilotPlan.getWeatherDaytime() ? weatherClearDay : weatherClearNight; } p.drawPixmap(weatherRect, icon->currentPixmap()); p.restore(); }