openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

687 lines
20 KiB

#include "selfdrive/ui/qt/maps/map.h"
#include <eigen3/Eigen/Dense>
#include <cmath>
#include <QDebug>
#include <QFileInfo>
#include <QPainterPath>
#include "common/swaglog.h"
#include "common/transformations/coordinates.hpp"
#include "selfdrive/ui/qt/maps/map_helpers.h"
#include "selfdrive/ui/qt/request_repeater.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/ui.h"
const int PAN_TIMEOUT = 100;
const float MANEUVER_TRANSITION_THRESHOLD = 10;
const float MAX_ZOOM = 17;
const float MIN_ZOOM = 14;
const float MAX_PITCH = 50;
const float MIN_PITCH = 0;
const float MAP_SCALE = 2;
const float VALID_POS_STD = 50.0; // m
const QString ICON_SUFFIX = ".png";
MapWindow::MapWindow(const QMapboxGLSettings &settings) : m_settings(settings), velocity_filter(0, 10, 0.05) {
QObject::connect(uiState(), &UIState::uiUpdate, this, &MapWindow::updateState);
// Instructions
map_instructions = new MapInstructions(this);
QObject::connect(this, &MapWindow::instructionsChanged, map_instructions, &MapInstructions::updateInstructions);
QObject::connect(this, &MapWindow::distanceChanged, map_instructions, &MapInstructions::updateDistance);
map_instructions->setFixedWidth(width());
map_instructions->setVisible(false);
map_eta = new MapETA(this);
QObject::connect(this, &MapWindow::ETAChanged, map_eta, &MapETA::updateETA);
const int h = 120;
map_eta->setFixedHeight(h);
map_eta->move(25, 1080 - h - bdr_s*2);
map_eta->setVisible(false);
auto last_gps_position = coordinate_from_param("LastGPSPosition");
if (last_gps_position) {
last_position = *last_gps_position;
}
grabGesture(Qt::GestureType::PinchGesture);
qDebug() << "MapWindow initialized";
}
MapWindow::~MapWindow() {
makeCurrent();
}
void MapWindow::initLayers() {
// This doesn't work from initializeGL
if (!m_map->layerExists("modelPathLayer")) {
qDebug() << "Initializing modelPathLayer";
QVariantMap modelPath;
modelPath["id"] = "modelPathLayer";
modelPath["type"] = "line";
modelPath["source"] = "modelPathSource";
m_map->addLayer(modelPath);
m_map->setPaintProperty("modelPathLayer", "line-color", QColor("red"));
m_map->setPaintProperty("modelPathLayer", "line-width", 5.0);
m_map->setLayoutProperty("modelPathLayer", "line-cap", "round");
}
if (!m_map->layerExists("navLayer")) {
qDebug() << "Initializing navLayer";
QVariantMap nav;
nav["id"] = "navLayer";
nav["type"] = "line";
nav["source"] = "navSource";
m_map->addLayer(nav, "road-intersection");
m_map->setPaintProperty("navLayer", "line-color", QColor("#31a1ee"));
m_map->setPaintProperty("navLayer", "line-width", 7.5);
m_map->setLayoutProperty("navLayer", "line-cap", "round");
}
if (!m_map->layerExists("carPosLayer")) {
qDebug() << "Initializing carPosLayer";
m_map->addImage("label-arrow", QImage("../assets/images/triangle.svg"));
QVariantMap carPos;
carPos["id"] = "carPosLayer";
carPos["type"] = "symbol";
carPos["source"] = "carPosSource";
m_map->addLayer(carPos);
m_map->setLayoutProperty("carPosLayer", "icon-pitch-alignment", "map");
m_map->setLayoutProperty("carPosLayer", "icon-image", "label-arrow");
m_map->setLayoutProperty("carPosLayer", "icon-size", 0.5);
m_map->setLayoutProperty("carPosLayer", "icon-ignore-placement", true);
m_map->setLayoutProperty("carPosLayer", "icon-allow-overlap", true);
m_map->setLayoutProperty("carPosLayer", "symbol-sort-key", 0);
}
}
void MapWindow::updateState(const UIState &s) {
if (!uiState()->scene.started) {
return;
}
const SubMaster &sm = *(s.sm);
update();
if (sm.updated("liveLocationKalman")) {
auto locationd_location = sm["liveLocationKalman"].getLiveLocationKalman();
auto locationd_pos = locationd_location.getPositionGeodetic();
auto locationd_orientation = locationd_location.getCalibratedOrientationNED();
auto locationd_velocity = locationd_location.getVelocityCalibrated();
locationd_valid = (locationd_location.getStatus() == cereal::LiveLocationKalman::Status::VALID) &&
locationd_pos.getValid() && locationd_orientation.getValid() && locationd_velocity.getValid();
if (locationd_valid) {
last_position = QMapbox::Coordinate(locationd_pos.getValue()[0], locationd_pos.getValue()[1]);
last_bearing = RAD2DEG(locationd_orientation.getValue()[2]);
velocity_filter.update(locationd_velocity.getValue()[0]);
}
}
if (sm.updated("gnssMeasurements")) {
auto laikad_location = sm["gnssMeasurements"].getGnssMeasurements();
auto laikad_pos = laikad_location.getPositionECEF();
auto laikad_pos_ecef = laikad_pos.getValue();
auto laikad_pos_std = laikad_pos.getStd();
auto laikad_velocity_ecef = laikad_location.getVelocityECEF().getValue();
laikad_valid = laikad_pos.getValid() && Eigen::Vector3d(laikad_pos_std[0], laikad_pos_std[1], laikad_pos_std[2]).norm() < VALID_POS_STD;
if (laikad_valid && !locationd_valid) {
ECEF ecef = {.x = laikad_pos_ecef[0], .y = laikad_pos_ecef[1], .z = laikad_pos_ecef[2]};
Geodetic laikad_pos_geodetic = ecef2geodetic(ecef);
last_position = QMapbox::Coordinate(laikad_pos_geodetic.lat, laikad_pos_geodetic.lon);
// Compute NED velocity
LocalCoord converter(ecef);
ECEF next_ecef = {.x = ecef.x + laikad_velocity_ecef[0], .y = ecef.y + laikad_velocity_ecef[1], .z = ecef.z + laikad_velocity_ecef[2]};
Eigen::VectorXd ned_vel = converter.ecef2ned(next_ecef).to_vector() - converter.ecef2ned(ecef).to_vector();
float velocity = ned_vel.norm();
velocity_filter.update(velocity);
// Convert NED velocity to angle
if (velocity > 1.0) {
float new_bearing = fmod(RAD2DEG(atan2(ned_vel[1], ned_vel[0])) + 360.0, 360.0);
if (last_bearing) {
float delta = 0.1 * angle_difference(*last_bearing, new_bearing); // Smooth heading
last_bearing = fmod(*last_bearing + delta + 360.0, 360.0);
} else {
last_bearing = new_bearing;
}
}
}
}
if (sm.updated("navRoute") && sm["navRoute"].getNavRoute().getCoordinates().size()) {
qWarning() << "Got new navRoute from navd. Opening map:" << allow_open;
// Only open the map on setting destination the first time
if (allow_open) {
setVisible(true); // Show map on destination set/change
allow_open = false;
}
}
if (m_map.isNull()) {
return;
}
loaded_once = loaded_once || m_map->isFullyLoaded();
if (!loaded_once) {
map_instructions->showError(tr("Map Loading"));
return;
}
initLayers();
if (locationd_valid || laikad_valid) {
map_instructions->noError();
// Update current location marker
auto point = coordinate_to_collection(*last_position);
QMapbox::Feature feature1(QMapbox::Feature::PointType, point, {}, {});
QVariantMap carPosSource;
carPosSource["type"] = "geojson";
carPosSource["data"] = QVariant::fromValue<QMapbox::Feature>(feature1);
m_map->updateSource("carPosSource", carPosSource);
} else {
map_instructions->showError(tr("Waiting for GPS"));
}
if (pan_counter == 0) {
if (last_position) m_map->setCoordinate(*last_position);
if (last_bearing) m_map->setBearing(*last_bearing);
} else {
pan_counter--;
}
if (zoom_counter == 0) {
m_map->setZoom(util::map_val<float>(velocity_filter.x(), 0, 30, MAX_ZOOM, MIN_ZOOM));
} else {
zoom_counter--;
}
if (sm.updated("navInstruction")) {
if (sm.valid("navInstruction")) {
auto i = sm["navInstruction"].getNavInstruction();
emit ETAChanged(i.getTimeRemaining(), i.getTimeRemainingTypical(), i.getDistanceRemaining());
if (locationd_valid || laikad_valid) {
m_map->setPitch(MAX_PITCH); // TODO: smooth pitching based on maneuver distance
emit distanceChanged(i.getManeuverDistance()); // TODO: combine with instructionsChanged
emit instructionsChanged(i);
}
} else {
m_map->setPitch(MIN_PITCH);
clearRoute();
}
}
if (sm.rcv_frame("navRoute") != route_rcv_frame) {
qWarning() << "Updating navLayer with new route";
auto route = sm["navRoute"].getNavRoute();
auto route_points = capnp_coordinate_list_to_collection(route.getCoordinates());
QMapbox::Feature feature(QMapbox::Feature::LineStringType, route_points, {}, {});
QVariantMap navSource;
navSource["type"] = "geojson";
navSource["data"] = QVariant::fromValue<QMapbox::Feature>(feature);
m_map->updateSource("navSource", navSource);
m_map->setLayoutProperty("navLayer", "visibility", "visible");
route_rcv_frame = sm.rcv_frame("navRoute");
}
}
void MapWindow::resizeGL(int w, int h) {
m_map->resize(size() / MAP_SCALE);
map_instructions->setFixedWidth(width());
}
void MapWindow::initializeGL() {
m_map.reset(new QMapboxGL(this, m_settings, size(), 1));
if (last_position) {
m_map->setCoordinateZoom(*last_position, MAX_ZOOM);
} else {
m_map->setCoordinateZoom(QMapbox::Coordinate(64.31990695292795, -149.79038934046247), MIN_ZOOM);
}
m_map->setMargins({0, 350, 0, 50});
m_map->setPitch(MIN_PITCH);
m_map->setStyleUrl("mapbox://styles/commaai/ckr64tlwp0azb17nqvr9fj13s");
QObject::connect(m_map.data(), &QMapboxGL::mapChanged, [=](QMapboxGL::MapChange change) {
if (change == QMapboxGL::MapChange::MapChangeDidFinishLoadingMap) {
loaded_once = true;
}
});
}
void MapWindow::paintGL() {
if (!isVisible() || m_map.isNull()) return;
m_map->render();
}
void MapWindow::clearRoute() {
if (!m_map.isNull()) {
m_map->setLayoutProperty("navLayer", "visibility", "none");
m_map->setPitch(MIN_PITCH);
}
map_instructions->hideIfNoError();
map_eta->setVisible(false);
allow_open = true;
}
void MapWindow::mousePressEvent(QMouseEvent *ev) {
m_lastPos = ev->localPos();
ev->accept();
}
void MapWindow::mouseDoubleClickEvent(QMouseEvent *ev) {
if (last_position) m_map->setCoordinate(*last_position);
if (last_bearing) m_map->setBearing(*last_bearing);
m_map->setZoom(util::map_val<float>(velocity_filter.x(), 0, 30, MAX_ZOOM, MIN_ZOOM));
update();
pan_counter = 0;
zoom_counter = 0;
}
void MapWindow::mouseMoveEvent(QMouseEvent *ev) {
QPointF delta = ev->localPos() - m_lastPos;
if (!delta.isNull()) {
pan_counter = PAN_TIMEOUT;
m_map->moveBy(delta / MAP_SCALE);
update();
}
m_lastPos = ev->localPos();
ev->accept();
}
void MapWindow::wheelEvent(QWheelEvent *ev) {
if (ev->orientation() == Qt::Horizontal) {
return;
}
float factor = ev->delta() / 1200.;
if (ev->delta() < 0) {
factor = factor > -1 ? factor : 1 / factor;
}
m_map->scaleBy(1 + factor, ev->pos() / MAP_SCALE);
update();
zoom_counter = PAN_TIMEOUT;
ev->accept();
}
bool MapWindow::event(QEvent *event) {
if (event->type() == QEvent::Gesture) {
return gestureEvent(static_cast<QGestureEvent*>(event));
}
return QWidget::event(event);
}
bool MapWindow::gestureEvent(QGestureEvent *event) {
if (QGesture *pinch = event->gesture(Qt::PinchGesture)) {
pinchTriggered(static_cast<QPinchGesture *>(pinch));
}
return true;
}
void MapWindow::pinchTriggered(QPinchGesture *gesture) {
QPinchGesture::ChangeFlags changeFlags = gesture->changeFlags();
if (changeFlags & QPinchGesture::ScaleFactorChanged) {
// TODO: figure out why gesture centerPoint doesn't work
m_map->scaleBy(gesture->scaleFactor(), {width() / 2.0 / MAP_SCALE, height() / 2.0 / MAP_SCALE});
update();
zoom_counter = PAN_TIMEOUT;
}
}
void MapWindow::offroadTransition(bool offroad) {
if (offroad) {
clearRoute();
} else {
auto dest = coordinate_from_param("NavDestination");
setVisible(dest.has_value());
}
last_bearing = {};
}
MapInstructions::MapInstructions(QWidget * parent) : QWidget(parent) {
is_rhd = Params().getBool("IsRhdDetected");
QHBoxLayout *main_layout = new QHBoxLayout(this);
main_layout->setContentsMargins(11, 50, 11, 11);
{
QVBoxLayout *layout = new QVBoxLayout;
icon_01 = new QLabel;
layout->addWidget(icon_01);
layout->addStretch();
main_layout->addLayout(layout);
}
{
QVBoxLayout *layout = new QVBoxLayout;
distance = new QLabel;
distance->setStyleSheet(R"(font-size: 90px;)");
layout->addWidget(distance);
primary = new QLabel;
primary->setStyleSheet(R"(font-size: 60px;)");
primary->setWordWrap(true);
layout->addWidget(primary);
secondary = new QLabel;
secondary->setStyleSheet(R"(font-size: 50px;)");
secondary->setWordWrap(true);
layout->addWidget(secondary);
lane_widget = new QWidget;
lane_widget->setFixedHeight(125);
lane_layout = new QHBoxLayout(lane_widget);
layout->addWidget(lane_widget);
main_layout->addLayout(layout);
}
setStyleSheet(R"(
* {
color: white;
font-family: "Inter";
}
)");
QPalette pal = palette();
pal.setColor(QPalette::Background, QColor(0, 0, 0, 150));
setAutoFillBackground(true);
setPalette(pal);
}
void MapInstructions::updateDistance(float d) {
d = std::max(d, 0.0f);
QString distance_str;
if (uiState()->scene.is_metric) {
if (d > 500) {
distance_str.setNum(d / 1000, 'f', 1);
distance_str += tr(" km");
} else {
distance_str.setNum(50 * int(d / 50));
distance_str += tr(" m");
}
} else {
float miles = d * METER_TO_MILE;
float feet = d * METER_TO_FOOT;
if (feet > 500) {
distance_str.setNum(miles, 'f', 1);
distance_str += tr(" mi");
} else {
distance_str.setNum(50 * int(feet / 50));
distance_str += tr(" ft");
}
}
distance->setAlignment(Qt::AlignLeft);
distance->setText(distance_str);
}
void MapInstructions::showError(QString error_text) {
primary->setText("");
distance->setText(error_text);
distance->setAlignment(Qt::AlignCenter);
secondary->setVisible(false);
icon_01->setVisible(false);
this->error = true;
lane_widget->setVisible(false);
setVisible(true);
}
void MapInstructions::noError() {
error = false;
}
void MapInstructions::updateInstructions(cereal::NavInstruction::Reader instruction) {
// Word wrap widgets need fixed width
primary->setFixedWidth(width() - 250);
secondary->setFixedWidth(width() - 250);
// Show instruction text
QString primary_str = QString::fromStdString(instruction.getManeuverPrimaryText());
QString secondary_str = QString::fromStdString(instruction.getManeuverSecondaryText());
primary->setText(primary_str);
secondary->setVisible(secondary_str.length() > 0);
secondary->setText(secondary_str);
// Show arrow with direction
QString type = QString::fromStdString(instruction.getManeuverType());
QString modifier = QString::fromStdString(instruction.getManeuverModifier());
if (!type.isEmpty()) {
QString fn = "../assets/navigation/direction_" + type;
if (!modifier.isEmpty()) {
fn += "_" + modifier;
}
fn += ICON_SUFFIX;
fn = fn.replace(' ', '_');
// for rhd, reflect direction and then flip
if (is_rhd) {
if (fn.contains("left")) {
fn.replace(QString("left"), QString("right"));
} else if (fn.contains("right")) {
fn.replace(QString("right"), QString("left"));
}
}
QPixmap pix(fn);
if (is_rhd) {
pix = pix.transformed(QTransform().scale(-1, 1));
}
icon_01->setPixmap(pix.scaledToWidth(200, Qt::SmoothTransformation));
icon_01->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
icon_01->setVisible(true);
}
// Show lanes
bool has_lanes = false;
clearLayout(lane_layout);
for (auto const &lane: instruction.getLanes()) {
has_lanes = true;
bool active = lane.getActive();
// TODO: only use active direction if active
bool left = false, straight = false, right = false;
for (auto const &direction: lane.getDirections()) {
left |= direction == cereal::NavInstruction::Direction::LEFT;
right |= direction == cereal::NavInstruction::Direction::RIGHT;
straight |= direction == cereal::NavInstruction::Direction::STRAIGHT;
}
// TODO: Make more images based on active direction and combined directions
QString fn = "../assets/navigation/direction_";
if (left) {
fn += "turn_left";
} else if (right) {
fn += "turn_right";
} else if (straight) {
fn += "turn_straight";
}
if (!active) {
fn += "_inactive";
}
auto icon = new QLabel;
icon->setPixmap(loadPixmap(fn + ICON_SUFFIX, {125, 125}, Qt::IgnoreAspectRatio));
icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
lane_layout->addWidget(icon);
}
lane_widget->setVisible(has_lanes);
show();
resize(sizeHint());
}
void MapInstructions::hideIfNoError() {
if (!error) {
hide();
}
}
MapETA::MapETA(QWidget * parent) : QWidget(parent) {
QHBoxLayout *main_layout = new QHBoxLayout(this);
main_layout->setContentsMargins(40, 25, 40, 25);
{
QHBoxLayout *layout = new QHBoxLayout;
eta = new QLabel;
eta->setAlignment(Qt::AlignCenter);
eta->setStyleSheet("font-weight:600");
eta_unit = new QLabel;
eta_unit->setAlignment(Qt::AlignCenter);
layout->addWidget(eta);
layout->addWidget(eta_unit);
main_layout->addLayout(layout);
}
main_layout->addSpacing(40);
{
QHBoxLayout *layout = new QHBoxLayout;
time = new QLabel;
time->setAlignment(Qt::AlignCenter);
time_unit = new QLabel;
time_unit->setAlignment(Qt::AlignCenter);
layout->addWidget(time);
layout->addWidget(time_unit);
main_layout->addLayout(layout);
}
main_layout->addSpacing(40);
{
QHBoxLayout *layout = new QHBoxLayout;
distance = new QLabel;
distance->setAlignment(Qt::AlignCenter);
distance->setStyleSheet("font-weight:600");
distance_unit = new QLabel;
distance_unit->setAlignment(Qt::AlignCenter);
layout->addWidget(distance);
layout->addWidget(distance_unit);
main_layout->addLayout(layout);
}
setStyleSheet(R"(
* {
color: white;
font-family: "Inter";
font-size: 70px;
}
)");
QPalette pal = palette();
pal.setColor(QPalette::Background, QColor(0, 0, 0, 150));
setAutoFillBackground(true);
setPalette(pal);
}
void MapETA::updateETA(float s, float s_typical, float d) {
if (d < MANEUVER_TRANSITION_THRESHOLD) {
hide();
return;
}
// ETA
auto eta_time = QDateTime::currentDateTime().addSecs(s).time();
if (params.getBool("NavSettingTime24h")) {
eta->setText(eta_time.toString("HH:mm"));
eta_unit->setText(tr("eta"));
} else {
auto t = eta_time.toString("h:mm a").split(' ');
eta->setText(t[0]);
eta_unit->setText(t[1]);
}
// Remaining time
if (s < 3600) {
time->setText(QString::number(int(s / 60)));
time_unit->setText(tr("min"));
} else {
int hours = int(s) / 3600;
time->setText(QString::number(hours) + ":" + QString::number(int((s - hours * 3600) / 60)).rightJustified(2, '0'));
time_unit->setText(tr("hr"));
}
QString color;
if (s / s_typical > 1.5) {
color = "#DA3025";
} else if (s / s_typical > 1.2) {
color = "#DAA725";
} else {
color = "#25DA6E";
}
time->setStyleSheet(QString(R"(color: %1; font-weight:600;)").arg(color));
time_unit->setStyleSheet(QString(R"(color: %1;)").arg(color));
// Distance
QString distance_str;
float num = 0;
if (uiState()->scene.is_metric) {
num = d / 1000.0;
distance_unit->setText(tr("km"));
} else {
num = d * METER_TO_MILE;
distance_unit->setText(tr("mi"));
}
distance_str.setNum(num, 'f', num < 100 ? 1 : 0);
distance->setText(distance_str);
show();
adjustSize();
repaint();
adjustSize();
// Rounded corners
const int radius = 25;
const auto r = rect();
// Top corners rounded
QPainterPath path;
path.setFillRule(Qt::WindingFill);
path.addRoundedRect(r, radius, radius);
// Bottom corners not rounded
path.addRect(r.marginsRemoved(QMargins(0, radius, 0, 0)));
// Set clipping mask
QRegion mask = QRegion(path.simplified().toFillPolygon().toPolygon());
setMask(mask);
// Center
move(static_cast<QWidget*>(parent())->width() / 2 - width() / 2, 1080 - height() - bdr_s*2);
}