diff --git a/frogpilot/ui/qt/widgets/drive_summary.cc b/frogpilot/ui/qt/widgets/drive_summary.cc new file mode 100644 index 00000000..df6f5e51 --- /dev/null +++ b/frogpilot/ui/qt/widgets/drive_summary.cc @@ -0,0 +1,225 @@ +#include "selfdrive/ui/qt/widgets/scrollview.h" + +#include "frogpilot/ui/qt/widgets/drive_summary.h" + +FrogPilotDriveSummary::FrogPilotDriveSummary(QWidget *parent, bool randomEvents) : QFrame(parent), displayRandomEvents(randomEvents) { + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(20, 20, 20, 20); + mainLayout->setSpacing(15); + + titleLabel = new QLabel(randomEvents ? tr("Random Events Summary") : tr("Drive Summary"), this); + titleLabel->setAlignment(Qt::AlignCenter); + titleLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + titleLabel->setStyleSheet(R"( + QLabel { + background-color: #444444; + border-radius: 12px; + color: #FFFFFF; + font-size: 50px; + font-weight: bold; + padding: 12px 28px; + } + )"); + titleLabel->setMaximumHeight(titleLabel->sizeHint().height()); + + mainLayout->addWidget(titleLabel); + mainLayout->addSpacing(10); + + QWidget *containerWidget = new QWidget(this); + QVBoxLayout *listLayout = new QVBoxLayout(containerWidget); + listLayout->setAlignment(Qt::AlignTop); + listLayout->setSpacing(20); + + if (displayRandomEvents) { + randomEventsMap.insert("accel30", tr("UwUs")); + randomEventsMap.insert("accel35", tr("Loch Ness Encounters")); + randomEventsMap.insert("accel40", tr("Visits to 1955")); + randomEventsMap.insert("dejaVuCurve", tr("Deja Vu Moments")); + randomEventsMap.insert("firefoxSteerSaturated", tr("Internet Explorer Weeeeeeees")); + randomEventsMap.insert("hal9000", tr("HAL 9000 Denials")); + randomEventsMap.insert("openpilotCrashedRandomEvent", tr("openpilot Crashes")); + randomEventsMap.insert("thisIsFineSteerSaturated", tr("This Is Fine Moments")); + randomEventsMap.insert("toBeContinued", tr("To Be Continued Moments")); + randomEventsMap.insert("vCruise69", tr("Noices")); + randomEventsMap.insert("yourFrogTriedToKillMe", tr("Attempted Frog Murders")); + randomEventsMap.insert("youveGotMail", tr("Total Mail Received")); + } else { + listLayout->addWidget(createStatBox(tr("% of Drive With openpilot Engaged"), &engagementValue, this)); + listLayout->addWidget(createStatBox(tr("Drive Distance"), &frogPilotMetersValue, this)); + listLayout->addWidget(createStatBox(tr("Drive Time"), &trackedTimeValue, this)); + listLayout->addWidget(createStatBox(tr("% of Drive In \"Experimental Mode\""), &experimentalModeTimeValue, this)); + } + + if (displayRandomEvents) { + eventsListLayout = listLayout; + mainLayout->addWidget(new ScrollView(containerWidget, this), 1); + } else { + mainLayout->addWidget(containerWidget, 1); + } + + setLayout(mainLayout); + + setStyleSheet(R"( + QFrame { + background-color: #333333; + } + )"); + + QObject::connect(device(), &Device::interactiveTimeout, [this]() { + emit panelClosed(); + }); + QObject::connect(uiState(), &UIState::offroadTransition, [this](bool offroad) { + if (!offroad) { + previousStats = QJsonDocument::fromJson(QString::fromStdString(params.get("FrogPilotStats")).toUtf8()).object(); + } + }); +} + +void FrogPilotDriveSummary::mousePressEvent(QMouseEvent *e) { + emit panelClosed(); +} + +void FrogPilotDriveSummary::showEvent(QShowEvent *event) { + bool isMetric = params.getBool("IsMetric"); + + QJsonObject currentStats = QJsonDocument::fromJson(QString::fromStdString(params.get("FrogPilotStats")).toUtf8()).object(); + + if (displayRandomEvents) { + QJsonObject currentRandomEvents = currentStats.value("RandomEvents").toObject(); + QJsonObject previousRandomEvents = previousStats.value("RandomEvents").toObject(); + + QList> eventsList; + for (QMap::const_iterator it = randomEventsMap.constBegin(); it != randomEventsMap.constEnd(); ++it) { + int currentValue = currentRandomEvents.value(it.key()).toInt(); + int previousValue = previousRandomEvents.value(it.key()).toInt(); + int diffValue = currentValue - previousValue; + + if (diffValue > 0) { + eventsList.append(qMakePair(it.value(), diffValue)); + } + } + + std::sort(eventsList.begin(), eventsList.end(), [](const QPair &a, const QPair &b) { + if (a.second != b.second) { + return a.second > b.second; + } else { + return a.first.localeAwareCompare(b.first) < 0; + } + }); + + QLayoutItem *child; + while ((child = eventsListLayout->takeAt(0)) != nullptr) { + if (QWidget *widget = child->widget()) { + widget->deleteLater(); + } + delete child; + } + randomEventLabels.clear(); + + if (eventsList.isEmpty()) { + eventsListLayout->setAlignment(Qt::AlignCenter); + + QLabel *noEventsLabel = new QLabel(tr("No Random Events Played!"), this); + noEventsLabel->setAlignment(Qt::AlignCenter); + noEventsLabel->setStyleSheet(R"( + QLabel { + font-size: 50px; + font-weight: bold; + color: #FFFFFF; + } + )"); + eventsListLayout->addWidget(noEventsLabel); + } else { + eventsListLayout->setAlignment(Qt::AlignTop); + + for (QList>::const_iterator it = eventsList.constBegin(); it != eventsList.constEnd(); ++it) { + QLabel *valueLabel = nullptr; + eventsListLayout->addWidget(createStatBox(it->first, &valueLabel, this)); + randomEventLabels.insert(it->first, valueLabel); + valueLabel->setText(QLocale().toString(it->second)); + } + } + } else { + std::function diffDouble = [currentStats, this](const QString &key) -> double { + return currentStats.value(key).toDouble() - previousStats.value(key).toDouble(); + }; + + std::function formatDistance = [&](double meters) { + double value; + QString unit; + if (isMetric) { + value = meters / 1000.0; + unit = (qRound(value) == 1) ? tr(" kilometer") : tr(" kilometers"); + } else { + value = meters * METER_TO_MILE; + unit = (qRound(value) == 1) ? tr(" mile") : tr(" miles"); + } + return QLocale().toString(qRound(value)) + unit; + }; + + std::function formatTime = [&](int seconds) { + static int secondsInDay = 60 * 60 * 24; + static int secondsInHour = 60 * 60; + + int days = seconds / secondsInDay; + int hours = (seconds % secondsInDay) / secondsInHour; + int minutes = (seconds % secondsInHour) / 60; + + QString result; + if (days > 0) result += QLocale().toString(days) + (days == 1 ? tr(" day ") : tr(" days ")); + if (hours > 0 || days > 0) result += QLocale().toString(hours) + (hours == 1 ? tr(" hour ") : tr(" hours ")); + result += QLocale().toString(minutes) + (minutes == 1 ? tr(" minute") : tr(" minutes")); + return result.trimmed(); + }; + + int engagedTime = diffDouble("AOLTime") + diffDouble("LongitudinalTime"); + int experimentalTime = diffDouble("ExperimentalModeTime"); + int trackedTime = diffDouble("TrackedTime"); + + engagementValue->setText(QLocale().toString((trackedTime > 0) ? (engagedTime * 100 / trackedTime) : 0) + "%"); + experimentalModeTimeValue->setText(QLocale().toString((trackedTime > 0) ? (experimentalTime * 100 / trackedTime) : 0) + "%"); + frogPilotMetersValue->setText(formatDistance(diffDouble("FrogPilotMeters"))); + trackedTimeValue->setText(formatTime(trackedTime)); + } +} + +void FrogPilotDriveSummary::hideEvent(QHideEvent *event) { + emit panelClosed(); +} + +QWidget *FrogPilotDriveSummary::createStatBox(const QString &title, QLabel **valueLabel, QWidget *parent) { + QWidget *box = new QWidget(parent); + + QVBoxLayout *layout = new QVBoxLayout(box); + layout->setAlignment(Qt::AlignCenter); + layout->setContentsMargins(10, 10, 10, 10); + layout->setSpacing(8); + + QLabel *statTitleLabel = new QLabel(title, box); + statTitleLabel->setAlignment(Qt::AlignCenter); + statTitleLabel->setStyleSheet(R"( + QLabel { + color: #AAAAAA; + font-size: 40px; + font-weight: bold; + } + )"); + + QLabel *value = new QLabel("-", box); + value->setAlignment(Qt::AlignCenter); + value->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + value->setStyleSheet(R"( + QLabel { + color: #FFFFFF; + font-size: 75px; + font-weight: bold; + } + )"); + + *valueLabel = value; + + layout->addWidget(statTitleLabel); + layout->addWidget(value); + + return box; +} diff --git a/frogpilot/ui/qt/widgets/drive_summary.h b/frogpilot/ui/qt/widgets/drive_summary.h new file mode 100644 index 00000000..bea16c4f --- /dev/null +++ b/frogpilot/ui/qt/widgets/drive_summary.h @@ -0,0 +1,39 @@ +#pragma once + +#include "selfdrive/ui/ui.h" + +class FrogPilotDriveSummary : public QFrame { + Q_OBJECT + +public: + explicit FrogPilotDriveSummary(QWidget *parent = nullptr, bool random_events = false); + +signals: + void panelClosed(); + +protected: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void mousePressEvent(QMouseEvent *e); + +private: + QWidget *createStatBox(const QString &title, QLabel **valueLabel, QWidget *parent); + + bool displayRandomEvents; + + Params params; + + QJsonObject previousStats; + + QLabel *experimentalModeTimeValue; + QLabel *frogPilotMetersValue; + QLabel *engagementValue; + QLabel *titleLabel; + QLabel *trackedTimeValue; + + QMap randomEventLabels; + + QMap randomEventsMap; + + QVBoxLayout *eventsListLayout; +}; diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 487e0698..54b43881 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -49,6 +49,7 @@ frogpilot_src = ["../../frogpilot/ui/frogpilot_ui.cc", "../../frogpilot/ui/qt/of "../../frogpilot/ui/qt/offroad/wheel_settings.cc", "../../frogpilot/ui/qt/onroad/frogpilot_annotated_camera.cc", "../../frogpilot/ui/qt/onroad/frogpilot_buttons.cc", "../../frogpilot/ui/qt/onroad/frogpilot_onroad.cc", "../../frogpilot/ui/qt/widgets/developer_sidebar.cc", "../../frogpilot/ui/qt/widgets/drive_stats.cc", + "../../frogpilot/ui/qt/widgets/drive_summary.cc", "../../frogpilot/ui/qt/widgets/navigation_functions.cc"] qt_src += frogpilot_src diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index 80532608..1560870b 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -10,6 +10,7 @@ #include "selfdrive/ui/qt/widgets/prime.h" #include "frogpilot/ui/qt/widgets/drive_stats.h" +#include "frogpilot/ui/qt/widgets/drive_summary.h" // HomeWindow: the container for the offroad and onroad UIs @@ -170,20 +171,35 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { home_layout->setContentsMargins(0, 0, 0, 0); home_layout->setSpacing(30); - // left: stack of DriveStats + // left: stack of DriveStats / DriveSummary QWidget *left_widget = new QWidget(this); QStackedLayout *left_stack = new QStackedLayout(left_widget); left_stack->setContentsMargins(0, 0, 0, 0); left_stack->addWidget(new DriveStats()); + FrogPilotDriveSummary *drive_summary = new FrogPilotDriveSummary(this); + left_stack->addWidget(drive_summary); + + QObject::connect(drive_summary, &FrogPilotDriveSummary::panelClosed, [left_stack]() { + left_stack->setCurrentIndex(0); + }); + QObject::connect(uiState(), &UIState::offroadTransition, [left_stack](bool offroad) { + static bool previouslyOnroad = false; + if (offroad && previouslyOnroad) { + left_stack->setCurrentIndex(1); + } + previouslyOnroad = !offroad; + }); home_layout->addWidget(left_widget, 1); - // right: ExperimentalModeButton, SetupWidget - QWidget* right_widget = new QWidget(this); - QVBoxLayout* right_column = new QVBoxLayout(right_widget); - right_column->setContentsMargins(0, 0, 0, 0); + // right: ExperimentalModeButton, SetupWidget, Random Events Summary + QStackedWidget *right_widget = new QStackedWidget(this); right_widget->setFixedWidth(750); + + QWidget *default_right = new QWidget(this); + QVBoxLayout *right_column = new QVBoxLayout(default_right); + right_column->setContentsMargins(0, 0, 0, 0); right_column->setSpacing(30); ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); @@ -194,7 +210,24 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings); right_column->addWidget(setup_widget, 1); - home_layout->addWidget(right_widget, 1); + right_widget->addWidget(default_right); + + FrogPilotDriveSummary *random_events_summary = new FrogPilotDriveSummary(this, true); + right_widget->addWidget(random_events_summary); + right_widget->setCurrentIndex(0); + + QObject::connect(random_events_summary, &FrogPilotDriveSummary::panelClosed, [=]() { + right_widget->setCurrentIndex(0); + }); + QObject::connect(uiState(), &UIState::offroadTransition, [right_widget](bool offroad) { + static bool previouslyOnroad = false; + if (offroad && previouslyOnroad && frogpilotUIState()->frogpilot_scene.frogpilot_toggles.value("random_events").toBool()) { + right_widget->setCurrentIndex(1); + } + previouslyOnroad = !offroad; + }); + + home_layout->addWidget(right_widget, 0); } center_layout->addWidget(home_widget);