Files
StarPilot/selfdrive/ui/qt/maps/map_settings.cc
T
2025-09-27 12:00:00 -07:00

476 lines
17 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/maps/map_settings.h"
#include <utility>
#include <QApplication>
#include <QDebug>
#include "common/util.h"
#include "selfdrive/ui/qt/request_repeater.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
static void swap(QJsonValueRef v1, QJsonValueRef v2) { std::swap(v1, v2); }
static bool locationEqual(const QJsonValue &v1, const QJsonValue &v2) {
return v1["latitude"] == v2["latitude"] && v1["longitude"] == v2["longitude"];
}
static qint64 convertTimestampToEpoch(const QString &timestamp) {
QDateTime dt = QDateTime::fromString(timestamp, Qt::ISODate);
return dt.isValid() ? dt.toSecsSinceEpoch() : 0;
}
MapSettings::MapSettings(bool closeable, QWidget *parent) : QFrame(parent) {
setContentsMargins(0, 0, 0, 0);
setAttribute(Qt::WA_NoMousePropagation);
auto *frame = new QVBoxLayout(this);
frame->setContentsMargins(40, 40, 40, 0);
frame->setSpacing(0);
auto *heading_frame = new QHBoxLayout;
heading_frame->setContentsMargins(0, 0, 0, 0);
heading_frame->setSpacing(32);
{
if (closeable) {
auto *close_btn = new QPushButton("");
close_btn->setStyleSheet(R"(
QPushButton {
color: #FFFFFF;
font-size: 100px;
padding-bottom: 8px;
border 1px grey solid;
border-radius: 70px;
background-color: #292929;
font-weight: 500;
}
QPushButton:pressed {
background-color: #3B3B3B;
}
)");
close_btn->setFixedSize(140, 140);
QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closeSettings(); });
// TODO: read map_on_left from ui state
heading_frame->addWidget(close_btn);
}
auto *heading = new QVBoxLayout;
heading->setContentsMargins(0, 0, 0, 0);
heading->setSpacing(16);
{
auto *title = new QLabel(tr("NAVIGATION"), this);
title->setStyleSheet("color: #FFFFFF; font-size: 54px; font-weight: 600;");
heading->addWidget(title);
subtitle = new QLabel(tr("Manage at %1").arg(QString("%1:8082").arg(frogpilotUIState()->wifi->getIp4Address())), this);
subtitle->setStyleSheet("color: #A0A0A0; font-size: 40px; font-weight: 300;");
heading->addWidget(subtitle);
}
heading_frame->addLayout(heading, 1);
}
frame->addLayout(heading_frame);
frame->addSpacing(32);
current_widget = new DestinationWidget(this);
QObject::connect(current_widget, &DestinationWidget::actionClicked,
[]() { NavManager::instance()->setCurrentDestination({}); });
frame->addWidget(current_widget);
frame->addSpacing(32);
QWidget *destinations_container = new QWidget(this);
destinations_layout = new QVBoxLayout(destinations_container);
destinations_layout->setContentsMargins(0, 32, 0, 32);
destinations_layout->setSpacing(20);
destinations_layout->addWidget(home_widget = new DestinationWidget(this));
destinations_layout->addWidget(work_widget = new DestinationWidget(this));
QObject::connect(home_widget, &DestinationWidget::navigateTo, this, &MapSettings::navigateTo);
QObject::connect(work_widget, &DestinationWidget::navigateTo, this, &MapSettings::navigateTo);
destinations_layout->addStretch();
ScrollView *destinations_scroller = new ScrollView(destinations_container, this);
destinations_scroller->setFrameShape(QFrame::NoFrame);
frame->addWidget(destinations_scroller);
setStyleSheet("MapSettings { background-color: #333333; }");
QObject::connect(NavManager::instance(), &NavManager::updated, this, &MapSettings::refresh);
}
void MapSettings::showEvent(QShowEvent *event) {
refresh();
}
void MapSettings::refresh() {
if (!isVisible()) return;
setUpdatesEnabled(false);
auto get_w = [this](int i) {
auto w = i < widgets.size() ? widgets[i] : widgets.emplace_back(new DestinationWidget);
if (!w->parentWidget()) {
destinations_layout->insertWidget(destinations_layout->count() - 1, w);
QObject::connect(w, &DestinationWidget::navigateTo, this, &MapSettings::navigateTo);
}
return w;
};
const auto current_dest = NavManager::instance()->currentDestination();
if (!current_dest.isEmpty()) {
current_widget->set(current_dest, true);
} else {
current_widget->unset("", true);
}
home_widget->unset(NAV_FAVORITE_LABEL_HOME);
work_widget->unset(NAV_FAVORITE_LABEL_WORK);
QString favorite_destinations = QString::fromStdString(params.get("FavoriteDestinations"));
QJsonDocument favorite_destinations_doc = QJsonDocument::fromJson(favorite_destinations.toUtf8());
std::optional<QJsonObject> home_favorite;
std::optional<QJsonObject> work_favorite;
QJsonArray other_favorites;
if (favorite_destinations_doc.isArray()) {
QJsonArray favorite_destinations_array = favorite_destinations_doc.array();
for (const QJsonValue &favorite_value : favorite_destinations_array) {
QJsonObject favorite_object = favorite_value.toObject();
if (favorite_object.contains("latitude") && favorite_object.contains("longitude")) {
bool is_home = favorite_object.value("is_home").toBool();
bool is_work = favorite_object.value("is_work").toBool();
if (!home_favorite.has_value() && is_home) {
home_favorite = favorite_object;
} else if (!work_favorite.has_value() && is_work) {
work_favorite = favorite_object;
} else if (!is_home && !is_work) {
other_favorites.append(favorite_object);
}
}
}
}
if (home_favorite.has_value()) {
QJsonObject home_destination;
home_destination.insert("latitude", home_favorite->value("latitude"));
home_destination.insert("longitude", home_favorite->value("longitude"));
if (home_favorite->contains("name")) {
home_destination.insert("place_name", home_favorite->value("name"));
home_destination.insert("place_details", home_favorite->value("name"));
}
home_destination.insert("save_type", NAV_TYPE_FAVORITE);
home_destination.insert("is_home", true);
home_widget->set(home_destination, false);
}
if (work_favorite.has_value()) {
QJsonObject work_destination;
work_destination.insert("latitude", work_favorite->value("latitude"));
work_destination.insert("longitude", work_favorite->value("longitude"));
if (work_favorite->contains("name")) {
work_destination.insert("place_name", work_favorite->value("name"));
work_destination.insert("place_details", work_favorite->value("name"));
}
work_destination.insert("save_type", NAV_TYPE_FAVORITE);
work_destination.insert("is_work", true);
work_widget->set(work_destination, false);
}
int n = 0;
for (const QJsonValue &favorite_value : other_favorites) {
QJsonObject favorite_object = favorite_value.toObject();
QJsonObject destination;
destination.insert("latitude", favorite_object.value("latitude"));
destination.insert("longitude", favorite_object.value("longitude"));
if (favorite_object.contains("name")) {
destination.insert("place_name", favorite_object.value("name"));
destination.insert("place_details", favorite_object.value("name"));
}
destination.insert("save_type", NAV_TYPE_FAVORITE);
DestinationWidget *destination_widget = get_w(n++);
destination_widget->set(destination, false);
}
for (QJsonValue location_value : NavManager::instance()->currentLocations()) {
DestinationWidget *destination_widget = nullptr;
QJsonObject destination = location_value.toObject();
if (destination["save_type"].toString() == NAV_TYPE_FAVORITE) {
QString label = destination["label"].toString();
if (label == NAV_FAVORITE_LABEL_HOME) {
destination_widget = home_widget;
}
if (label == NAV_FAVORITE_LABEL_WORK) {
destination_widget = work_widget;
}
}
destination_widget = destination_widget ? destination_widget : get_w(n++);
destination_widget->set(destination, false);
destination_widget->setVisible(!locationEqual(destination, current_dest));
}
setUpdatesEnabled(true);
subtitle->setText(tr("Manage at %1").arg(QString("%1:8082").arg(frogpilotUIState()->wifi->getIp4Address())));
}
void MapSettings::navigateTo(const QJsonObject &place) {
NavManager::instance()->setCurrentDestination(place);
emit closeSettings();
}
DestinationWidget::DestinationWidget(QWidget *parent) : QPushButton(parent) {
setContentsMargins(0, 0, 0, 0);
auto *frame = new QHBoxLayout(this);
frame->setContentsMargins(32, 24, 32, 24);
frame->setSpacing(32);
icon = new QLabel(this);
icon->setAlignment(Qt::AlignCenter);
icon->setFixedSize(96, 96);
icon->setObjectName("icon");
frame->addWidget(icon);
auto *inner_frame = new QVBoxLayout;
inner_frame->setContentsMargins(0, 0, 0, 0);
inner_frame->setSpacing(0);
{
title = new ElidedLabel(this);
title->setAttribute(Qt::WA_TransparentForMouseEvents);
inner_frame->addWidget(title);
subtitle = new ElidedLabel(this);
subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
subtitle->setObjectName("subtitle");
inner_frame->addWidget(subtitle);
}
frame->addLayout(inner_frame, 1);
action = new QPushButton(this);
action->setFixedSize(96, 96);
action->setObjectName("action");
action->setStyleSheet("font-size: 65px; font-weight: 600;");
QObject::connect(action, &QPushButton::clicked, this, &QPushButton::clicked);
QObject::connect(action, &QPushButton::clicked, this, &DestinationWidget::actionClicked);
frame->addWidget(action);
setFixedHeight(164);
setStyleSheet(R"(
DestinationWidget { background-color: #202123; border-radius: 10px; }
QLabel { color: #FFFFFF; font-size: 48px; font-weight: 400; }
#icon { background-color: #3B4356; border-radius: 48px; }
#subtitle { color: #9BA0A5; }
#action { border: none; border-radius: 48px; color: #FFFFFF; padding-bottom: 4px; }
/* current destination */
[current="true"] { background-color: #E8E8E8; }
[current="true"] QLabel { color: #000000; }
[current="true"] #icon { background-color: #42906B; }
[current="true"] #subtitle { color: #333333; }
[current="true"] #action { color: #202123; }
/* no saved destination */
[set="false"] QLabel { color: #9BA0A5; }
[current="true"][set="false"] QLabel { color: #A0000000; }
/* pressed */
[current="false"]:pressed { background-color: #18191B; }
[current="true"] #action:pressed { background-color: #D6D6D6; }
)");
QObject::connect(this, &QPushButton::clicked, [this]() { if (!dest.isEmpty()) emit navigateTo(dest); });
}
void DestinationWidget::set(const QJsonObject &destination, bool current) {
if (dest == destination) return;
dest = destination;
setProperty("current", current);
setProperty("set", true);
auto icon_pixmap = current ? icons().directions : icons().recent;
auto title_text = destination["place_name"].toString();
auto subtitle_text = destination["place_details"].toString();
bool is_fav = destination["save_type"] == NAV_TYPE_FAVORITE;
bool is_home = destination.value("is_home").toBool() || destination.value("label").toString() == NAV_FAVORITE_LABEL_HOME;
bool is_work = destination.value("is_work").toBool() || destination.value("label").toString() == NAV_FAVORITE_LABEL_WORK;
if (is_fav) {
if (is_home) {
icon_pixmap = icons().home;
title_text = tr("Home");
} else if (is_work) {
icon_pixmap = icons().work;
title_text = tr("Work");
} else {
icon_pixmap = icons().favorite;
}
}
icon->setPixmap(icon_pixmap);
title->setText(title_text);
subtitle->setText(subtitle_text);
subtitle->setVisible(true);
// TODO: use pixmap
action->setAttribute(Qt::WA_TransparentForMouseEvents, !current);
action->setText(current ? "×" : "");
action->setVisible(true);
setStyleSheet(styleSheet());
}
void DestinationWidget::unset(const QString &label, bool current) {
dest = {};
setProperty("current", current);
setProperty("set", false);
if (label.isEmpty()) {
icon->setPixmap(icons().directions);
title->setText(tr("No destination set"));
} else {
QString title_text = label == NAV_FAVORITE_LABEL_HOME ? tr("home") : tr("work");
icon->setPixmap(label == NAV_FAVORITE_LABEL_HOME ? icons().home : icons().work);
title->setText(tr("No %1 location set").arg(title_text));
}
subtitle->setVisible(false);
action->setVisible(false);
setStyleSheet(styleSheet());
setVisible(true);
}
// singleton NavManager
NavManager *NavManager::instance() {
static NavManager *request = new NavManager(qApp);
return request;
}
NavManager::NavManager(QObject *parent) : QObject(parent) {
locations = QJsonDocument::fromJson(params.get("NavPastDestinations").c_str()).array();
current_dest = QJsonDocument::fromJson(params.get("NavDestination").c_str()).object();
if (auto dongle_id = getDongleId()) {
{
// Fetch favorite and recent locations
QString url = CommaApi::BASE_URL + "/v1/navigation/" + *dongle_id + "/locations";
RequestRepeater *repeater = new RequestRepeater(this, url, "ApiCache_NavDestinations", 30, true);
QObject::connect(repeater, &RequestRepeater::requestDone, this, &NavManager::parseLocationsResponse);
}
{
auto param_watcher = new ParamWatcher(this);
QObject::connect(param_watcher, &ParamWatcher::paramChanged, this, &NavManager::updated);
// Destination set while offline
QString url = CommaApi::BASE_URL + "/v1/navigation/" + *dongle_id + "/next";
HttpRequest *deleter = new HttpRequest(this);
RequestRepeater *repeater = new RequestRepeater(this, url, "", 10, true);
QObject::connect(repeater, &RequestRepeater::requestDone, [=](const QString &resp, bool success) {
if (success && resp != "null") {
if (params.get("NavDestination").empty()) {
qWarning() << "Setting NavDestination from /next" << resp;
params.put("NavDestination", resp.toStdString());
} else {
qWarning() << "Got location from /next, but NavDestination already set";
}
// Send DELETE to clear destination server side
deleter->sendRequest(url, HttpRequest::Method::DELETE);
}
// athena can set destination at any time
param_watcher->addParam("NavDestination");
current_dest = QJsonDocument::fromJson(params.get("NavDestination").c_str()).object();
emit updated();
});
}
}
}
void NavManager::parseLocationsResponse(const QString &response, bool success) {
if (!success || response == prev_response) return;
prev_response = response;
QString trimmed = response.trimmed();
if (trimmed.isEmpty() || trimmed == "null") {
locations = QJsonArray();
emit updated();
return;
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(trimmed.toUtf8(), &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "JSON Parse failed on navigation locations" << response;
return;
}
QJsonArray remote_locations;
if (doc.isArray()) {
remote_locations = doc.array();
} else if (doc.isObject() && doc.object().value("locations").isArray()) {
remote_locations = doc.object().value("locations").toArray();
} else {
locations = QJsonArray();
emit updated();
return;
}
for (QJsonValueRef loc : remote_locations) {
QJsonObject obj = loc.toObject();
qint64 server_time = convertTimestampToEpoch(obj.value("modified").toString());
obj.insert("time", qMax(server_time, getLastActivity(obj)));
loc = obj;
}
locations = remote_locations;
sortLocations();
emit updated();
}
void NavManager::sortLocations() {
// Sort: alphabetical FAVORITES, and then most recent.
// We don't need to care about the ordering of HOME and WORK. DestinationWidget always displays them at the top.
std::stable_sort(locations.begin(), locations.end(), [](const QJsonValue &a, const QJsonValue &b) {
if (a["save_type"] == NAV_TYPE_FAVORITE || b["save_type"] == NAV_TYPE_FAVORITE) {
return (std::tuple(a["save_type"].toString(), a["place_name"].toString()) <
std::tuple(b["save_type"].toString(), b["place_name"].toString()));
} else {
return a["time"].toVariant().toLongLong() > b["time"].toVariant().toLongLong();
}
});
write_param_future = std::async(std::launch::async, [destinations = QJsonArray(locations)]() {
Params().put("NavPastDestinations", QJsonDocument(destinations).toJson().toStdString());
});
}
qint64 NavManager::getLastActivity(const QJsonObject &loc) const {
qint64 last_activity = 0;
auto it = std::find_if(locations.begin(), locations.end(),
[&loc](const QJsonValue &l) { return locationEqual(loc, l); });
if (it != locations.end()) {
auto tm = it->toObject().value("time");
if (!tm.isUndefined() && !tm.isNull()) {
last_activity = tm.toVariant().toLongLong();
}
}
return last_activity;
}
void NavManager::setCurrentDestination(const QJsonObject &loc) {
current_dest = loc;
if (!current_dest.isEmpty()) {
current_dest["time"] = QDateTime::currentSecsSinceEpoch();
auto it = std::find_if(locations.begin(), locations.end(),
[&loc](const QJsonValue &l) { return locationEqual(loc, l); });
if (it != locations.end()) {
*it = current_dest;
sortLocations();
}
params.put("NavDestination", QJsonDocument(current_dest).toJson().toStdString());
} else {
params.remove("NavDestination");
}
emit updated();
}