diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index ce61c094bd6bd73..12a579c5736fff5 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -174,17 +174,24 @@ void NvgWindow::updateState(const UIState &s) { const SubMaster &sm = *(s.sm); const auto cs = sm["controlsState"].getControlsState(); - float maxspeed = cs.getVCruise(); - bool cruise_set = maxspeed > 0 && (int)maxspeed != SET_SPEED_NA; + float set_speed = cs.getVCruise(); + bool cruise_set = set_speed > 0 && (int)set_speed != SET_SPEED_NA; if (cruise_set && !s.scene.is_metric) { - maxspeed *= KM_TO_MILE; + set_speed *= KM_TO_MILE; } - QString maxspeed_str = cruise_set ? QString::number(std::nearbyint(maxspeed)) : "N/A"; float cur_speed = std::max(0.0, sm["carState"].getCarState().getVEgo() * (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH)); + auto speed_limit_sign = sm["navInstruction"].getNavInstruction().getSpeedLimitSign(); + float speed_limit = sm["navInstruction"].getValid() ? sm["navInstruction"].getNavInstruction().getSpeedLimit() : 0.0; + speed_limit *= (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH); + + setProperty("speedLimit", speed_limit); + setProperty("has_us_speed_limit", speed_limit > 1 && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD); + setProperty("has_eu_speed_limit", speed_limit > 1 && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); + setProperty("is_cruise_set", cruise_set); - setProperty("speed", QString::number(std::nearbyint(cur_speed))); - setProperty("maxSpeed", maxspeed_str); + setProperty("speed", cur_speed); + setProperty("setSpeed", set_speed); setProperty("speedUnit", s.scene.is_metric ? "km/h" : "mph"); setProperty("hideDM", cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE); setProperty("status", s.status); @@ -205,26 +212,139 @@ void NvgWindow::drawHud(QPainter &p) { bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0)); p.fillRect(0, 0, width(), header_h, bg); - // max speed - QRect rc(bdr_s * 2, bdr_s * 1.5, 184, 202); - p.setPen(QPen(QColor(0xff, 0xff, 0xff, 100), 10)); - p.setBrush(QColor(0, 0, 0, 100)); - p.drawRoundedRect(rc, 20, 20); - p.setPen(Qt::NoPen); + QString speedLimitStr = QString::number(std::nearbyint(speedLimit)); + QString speedStr = QString::number(std::nearbyint(speed)); + QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed)) : "–"; + + // Draw outer box + border to contain set speed and speed limit + int default_rect_width = 172; + int rect_width = default_rect_width; + if (has_us_speed_limit && speedLimitStr.size() >= 3) rect_width = 223; + else if (has_eu_speed_limit) rect_width = 200; + + int rect_height = 204; + if (has_us_speed_limit) rect_height = 402; + else if (has_eu_speed_limit) rect_height = 392; - configFont(p, "Open Sans", 48, "Regular"); - drawText(p, rc.center().x(), 118, "MAX", is_cruise_set ? 200 : 100); + int top_radius = 32; + int bottom_radius = has_eu_speed_limit ? 100 : 32; + + QRect set_speed_rect(60 + default_rect_width / 2 - rect_width / 2, 45, rect_width, rect_height); + p.setPen(QPen(QColor(255, 255, 255, 75), 6)); + p.setBrush(QColor(0, 0, 0, 166)); + drawRoundedRect(p, set_speed_rect, top_radius, top_radius, bottom_radius, bottom_radius); + + // Draw set speed if (is_cruise_set) { - configFont(p, "Open Sans", 88, "Bold"); - drawText(p, rc.center().x(), 212, maxSpeed, 255); + if (speedLimit > 0 && status != STATUS_DISENGAGED && status != STATUS_OVERRIDE) { + p.setPen(interpColor( + setSpeed, + {speedLimit + 5, speedLimit + 15, speedLimit + 25}, + {QColor(0xff, 0xff, 0xff, 0xff), QColor(0xff, 0x95, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff)} + )); + } else { + p.setPen(QColor(0xff, 0xff, 0xff, 0xff)); + } } else { - configFont(p, "Open Sans", 80, "SemiBold"); - drawText(p, rc.center().x(), 212, maxSpeed, 100); + p.setPen(QColor(0x72, 0x72, 0x72, 0xff)); + } + configFont(p, "Open Sans", 90, "Bold"); + QRect speed_rect = getTextRect(p, Qt::AlignCenter, setSpeedStr); + speed_rect.moveCenter({set_speed_rect.center().x(), 0}); + speed_rect.moveTop(set_speed_rect.top() + 8); + p.drawText(speed_rect, Qt::AlignCenter, setSpeedStr); + + // Draw MAX + if (is_cruise_set) { + if (status == STATUS_DISENGAGED) { + p.setPen(QColor(0xff, 0xff, 0xff, 0xff)); + } else if (status == STATUS_OVERRIDE) { + p.setPen(QColor(0x91, 0x9b, 0x95, 0xff)); + } else if (speedLimit > 0) { + p.setPen(interpColor( + setSpeed, + {speedLimit + 5, speedLimit + 15, speedLimit + 25}, + {QColor(0x80, 0xd8, 0xa6, 0xff), QColor(0xff, 0xe4, 0xbf, 0xff), QColor(0xff, 0xbf, 0xbf, 0xff)} + )); + } else { + p.setPen(QColor(0x80, 0xd8, 0xa6, 0xff)); + } + } else { + p.setPen(QColor(0xa6, 0xa6, 0xa6, 0xff)); + } + configFont(p, "Open Sans", 40, "SemiBold"); + QRect max_rect = getTextRect(p, Qt::AlignCenter, "MAX"); + max_rect.moveCenter({set_speed_rect.center().x(), 0}); + max_rect.moveTop(set_speed_rect.top() + 123); + p.drawText(max_rect, Qt::AlignCenter, "MAX"); + + // US/Canada (MUTCD style) sign + if (has_us_speed_limit) { + const int border_width = 6; + const int sign_width = (speedLimitStr.size() >= 3) ? 199 : 148; + const int sign_height = 186; + + // White outer square + QRect sign_rect_outer(set_speed_rect.left() + 12, set_speed_rect.bottom() - 11 - sign_height, sign_width, sign_height); + p.setPen(Qt::NoPen); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawRoundedRect(sign_rect_outer, 24, 24); + + // Smaller white square with black border + QRect sign_rect(sign_rect_outer.left() + 1.5 * border_width, sign_rect_outer.top() + 1.5 * border_width, sign_width - 3 * border_width, sign_height - 3 * border_width); + p.setPen(QPen(QColor(0, 0, 0, 255), border_width)); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawRoundedRect(sign_rect, 16, 16); + + // "SPEED" + configFont(p, "Open Sans", 28, "SemiBold"); + QRect text_speed_rect = getTextRect(p, Qt::AlignCenter, "SPEED"); + text_speed_rect.moveCenter({sign_rect.center().x(), 0}); + text_speed_rect.moveTop(sign_rect_outer.top() + 20); + p.drawText(text_speed_rect, Qt::AlignCenter, "SPEED"); + + // "LIMIT" + QRect text_limit_rect = getTextRect(p, Qt::AlignCenter, "LIMIT"); + text_limit_rect.moveCenter({sign_rect.center().x(), 0}); + text_limit_rect.moveTop(sign_rect_outer.top() + 48); + p.drawText(text_limit_rect, Qt::AlignCenter, "LIMIT"); + + // Speed limit value + configFont(p, "Open Sans", 70, "Bold"); + QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); + speed_limit_rect.moveCenter({sign_rect.center().x(), 0}); + speed_limit_rect.moveTop(sign_rect.top() + 70); + p.drawText(speed_limit_rect, Qt::AlignCenter, speedLimitStr); + } + + // EU (Vienna style) sign + if (has_eu_speed_limit) { + int outer_radius = 176 / 2; + int inner_radius_1 = outer_radius - 6; // White outer border + int inner_radius_2 = inner_radius_1 - 20; // Red circle + + // Draw white circle with red border + QPoint center(set_speed_rect.center().x() + 1, set_speed_rect.top() + 204 + outer_radius); + p.setPen(Qt::NoPen); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawEllipse(center, outer_radius, outer_radius); + p.setBrush(QColor(255, 0, 0, 255)); + p.drawEllipse(center, inner_radius_1, inner_radius_1); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawEllipse(center, inner_radius_2, inner_radius_2); + + // Speed limit value + int font_size = (speedLimitStr.size() >= 3) ? 62 : 70; + configFont(p, "Open Sans", font_size, "Bold"); + QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); + speed_limit_rect.moveCenter(center); + p.setPen(QColor(0, 0, 0, 255)); + p.drawText(speed_limit_rect, Qt::AlignCenter, speedLimitStr); } // current speed configFont(p, "Open Sans", 176, "Bold"); - drawText(p, rect().center().x(), 210, speed); + drawText(p, rect().center().x(), 210, speedStr); configFont(p, "Open Sans", 66, "Regular"); drawText(p, rect().center().x(), 290, speedUnit, 200); @@ -243,9 +363,7 @@ void NvgWindow::drawHud(QPainter &p) { } void NvgWindow::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { - QFontMetrics fm(p.font()); - QRect init_rect = fm.boundingRect(text); - QRect real_rect = fm.boundingRect(init_rect, 0, text); + QRect real_rect = getTextRect(p, 0, text); real_rect.moveCenter({x, y - real_rect.height() / 2}); p.setPen(QColor(0xff, 0xff, 0xff, alpha)); diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 6ca2b3c738506cc..3d90a63d7c100a0 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -27,10 +27,14 @@ class OnroadAlerts : public QWidget { // container window for the NVG UI class NvgWindow : public CameraViewWidget { Q_OBJECT - Q_PROPERTY(QString speed MEMBER speed); + Q_PROPERTY(float speed MEMBER speed); Q_PROPERTY(QString speedUnit MEMBER speedUnit); - Q_PROPERTY(QString maxSpeed MEMBER maxSpeed); + Q_PROPERTY(float setSpeed MEMBER setSpeed); + Q_PROPERTY(float speedLimit MEMBER speedLimit); Q_PROPERTY(bool is_cruise_set MEMBER is_cruise_set); + Q_PROPERTY(bool has_eu_speed_limit MEMBER has_eu_speed_limit); + Q_PROPERTY(bool has_us_speed_limit MEMBER has_us_speed_limit); + Q_PROPERTY(bool engageable MEMBER engageable); Q_PROPERTY(bool dmActive MEMBER dmActive); Q_PROPERTY(bool hideDM MEMBER hideDM); @@ -48,13 +52,16 @@ class NvgWindow : public CameraViewWidget { QPixmap dm_img; const int radius = 192; const int img_size = (radius / 2) * 1.5; - QString speed; + float speed; QString speedUnit; - QString maxSpeed; + float setSpeed; + float speedLimit; bool is_cruise_set = false; bool engageable = false; bool dmActive = false; bool hideDM = false; + bool has_us_speed_limit = false; + bool has_eu_speed_limit = false; int status = STATUS_DISENGAGED; protected: diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index e40be0d46d43afb..aa3e4e787f0a7cf 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -138,3 +138,64 @@ QPixmap loadPixmap(const QString &fileName, const QSize &size, Qt::AspectRatioMo return QPixmap(fileName).scaled(size, aspectRatioMode, Qt::SmoothTransformation); } } + +QRect getTextRect(QPainter &p, int flags, QString text) { + QFontMetrics fm(p.font()); + QRect init_rect = fm.boundingRect(text); + return fm.boundingRect(init_rect, flags, text); +} + +void drawRoundedRect(QPainter &painter, const QRectF &rect, qreal xRadiusTop, qreal yRadiusTop, qreal xRadiusBottom, qreal yRadiusBottom){ + qreal w_2 = rect.width() / 2; + qreal h_2 = rect.height() / 2; + + xRadiusTop = 100 * qMin(xRadiusTop, w_2) / w_2; + yRadiusTop = 100 * qMin(yRadiusTop, h_2) / h_2; + + xRadiusBottom = 100 * qMin(xRadiusBottom, w_2) / w_2; + yRadiusBottom = 100 * qMin(yRadiusBottom, h_2) / h_2; + + qreal x = rect.x(); + qreal y = rect.y(); + qreal w = rect.width(); + qreal h = rect.height(); + + qreal rxx2Top = w*xRadiusTop/100; + qreal ryy2Top = h*yRadiusTop/100; + + qreal rxx2Bottom = w*xRadiusBottom/100; + qreal ryy2Bottom = h*yRadiusBottom/100; + + QPainterPath path; + path.arcMoveTo(x, y, rxx2Top, ryy2Top, 180); + path.arcTo(x, y, rxx2Top, ryy2Top, 180, -90); + path.arcTo(x+w-rxx2Top, y, rxx2Top, ryy2Top, 90, -90); + path.arcTo(x+w-rxx2Bottom, y+h-ryy2Bottom, rxx2Bottom, ryy2Bottom, 0, -90); + path.arcTo(x, y+h-ryy2Bottom, rxx2Bottom, ryy2Bottom, 270, -90); + path.closeSubpath(); + + painter.drawPath(path); +} + +QColor interpColor(float xv, std::vector xp, std::vector fp) { + assert(xp.size() == fp.size()); + + int N = xp.size(); + int hi = 0; + + while (hi < N and xv > xp[hi]) hi++; + int low = hi - 1; + + if (hi == N && xv > xp[low]) { + return fp[fp.size() - 1]; + } else if (hi == 0){ + return fp[0]; + } else { + return QColor( + (xv - xp[low]) * (fp[hi].red() - fp[low].red()) / (xp[hi] - xp[low]) + fp[low].red(), + (xv - xp[low]) * (fp[hi].green() - fp[low].green()) / (xp[hi] - xp[low]) + fp[low].green(), + (xv - xp[low]) * (fp[hi].blue() - fp[low].blue()) / (xp[hi] - xp[low]) + fp[low].blue(), + (xv - xp[low]) * (fp[hi].alpha() - fp[low].alpha()) / (xp[hi] - xp[low]) + fp[low].alpha() + ); + } +} diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 813777710a73bb9..9491c6798e18191 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -22,3 +22,7 @@ void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, co void initApp(int argc, char *argv[]); QWidget* topWidget (QWidget* widget); QPixmap loadPixmap(const QString &fileName, const QSize &size = {}, Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio); + +QRect getTextRect(QPainter &p, int flags, QString text); +void drawRoundedRect(QPainter &painter, const QRectF &rect, qreal xRadiusTop, qreal yRadiusTop, qreal xRadiusBottom, qreal yRadiusBottom); +QColor interpColor(float xv, std::vector xp, std::vector fp);