dragonpilot - 基於 openpilot 的開源駕駛輔助系統
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.

808 lines
29 KiB

#include "tools/cabana/chart/chart.h"
#include <QActionGroup>
#include <QApplication>
#include <QDrag>
#include <QGraphicsLayout>
#include <QGraphicsDropShadowEffect>
#include <QGraphicsItemGroup>
#include <QGraphicsOpacityEffect>
#include <QMenu>
#include <QMimeData>
#include <QOpenGLWidget>
#include <QPropertyAnimation>
#include <QRubberBand>
#include <QScreen>
#include <QtMath>
#include <QWindow>
#include "tools/cabana/chart/chartswidget.h"
// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html)
const int AXIS_X_TOP_MARGIN = 4;
static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; }
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent) : charts_widget(parent), tip_label(this), QChartView(nullptr, parent) {
series_type = (SeriesType)settings.chart_series_type;
QChart *chart = new QChart();
chart->setBackgroundVisible(false);
axis_x = new QValueAxis(this);
axis_y = new QValueAxis(this);
chart->addAxis(axis_x, Qt::AlignBottom);
chart->addAxis(axis_y, Qt::AlignLeft);
chart->legend()->layout()->setContentsMargins(0, 0, 0, 0);
chart->legend()->setShowToolTips(true);
chart->setMargins({0, 0, 0, 0});
axis_x->setRange(x_range.first, x_range.second);
setChart(chart);
createToolButtons();
// TODO: enable zoomIn/seekTo in live streaming mode.
setRubberBand(QChartView::HorizontalRubberBand);
setMouseTracking(true);
setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight);
signal_value_font.setPointSize(9);
QObject::connect(axis_y, &QValueAxis::rangeChanged, [this]() { resetChartCache(); });
QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, [this]() { resetChartCache(); });
QObject::connect(window()->windowHandle(), &QWindow::screenChanged, [this]() { resetChartCache(); });
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated);
QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved);
QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated);
}
void ChartView::createToolButtons() {
move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart());
move_icon->setToolTip(tr("Drag and drop to move chart"));
QToolButton *remove_btn = new ToolButton("x", tr("Remove Chart"));
close_btn_proxy = new QGraphicsProxyWidget(chart());
close_btn_proxy->setWidget(remove_btn);
close_btn_proxy->setZValue(chart()->zValue() + 11);
// series types
QMenu *menu = new QMenu(this);
auto change_series_group = new QActionGroup(menu);
change_series_group->setExclusive(true);
QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")};
for (int i = 0; i < types.size(); ++i) {
QAction *act = new QAction(types[i], change_series_group);
act->setData(i);
act->setCheckable(true);
act->setChecked(i == (int)series_type);
menu->addAction(act);
}
menu->addSeparator();
menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals);
split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); });
QToolButton *manage_btn = new ToolButton("list", "");
manage_btn->setMenu(menu);
manage_btn->setPopupMode(QToolButton::InstantPopup);
manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }");
manage_btn_proxy = new QGraphicsProxyWidget(chart());
manage_btn_proxy->setWidget(manage_btn);
manage_btn_proxy->setZValue(chart()->zValue() + 11);
QObject::connect(remove_btn, &QToolButton::clicked, [this]() { charts_widget->removeChart(this); });
QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) {
setSeriesType((SeriesType)action->data().toInt());
});
}
QSize ChartView::sizeHint() const {
return {CHART_MIN_WIDTH, settings.chart_height};
}
void ChartView::setTheme(QChart::ChartTheme theme) {
chart()->setTheme(theme);
if (theme == QChart::ChartThemeDark) {
axis_x->setTitleBrush(palette().text());
axis_x->setLabelsBrush(palette().text());
axis_y->setTitleBrush(palette().text());
axis_y->setLabelsBrush(palette().text());
chart()->legend()->setLabelColor(palette().color(QPalette::Text));
}
for (auto &s : sigs) {
s.series->setColor(getColor(s.sig));
}
}
void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) {
if (hasSignal(msg_id, sig)) return;
QXYSeries *series = createSeries(series_type, getColor(sig));
sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series});
updateSeries(sig);
updateSeriesPoints();
updateTitle();
emit charts_widget->seriesChanged();
}
bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const {
return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
}
void ChartView::removeIf(std::function<bool(const SigItem &s)> predicate) {
int prev_size = sigs.size();
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
if (predicate(*it)) {
chart()->removeSeries(it->series);
it->series->deleteLater();
it = sigs.erase(it);
} else {
++it;
}
}
if (sigs.empty()) {
charts_widget->removeChart(this);
} else if (sigs.size() != prev_size) {
emit charts_widget->seriesChanged();
updateAxisY();
resetChartCache();
}
}
void ChartView::signalUpdated(const cabana::Signal *sig) {
if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.sig == sig; })) {
updateTitle();
updateSeries(sig);
}
}
void ChartView::msgUpdated(MessageId id) {
if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id == id; }))
updateTitle();
}
void ChartView::manageSignals() {
SignalSelector dlg(tr("Mange Chart"), this);
for (auto &s : sigs) {
dlg.addSelected(s.msg_id, s.sig);
}
if (dlg.exec() == QDialog::Accepted) {
auto items = dlg.seletedItems();
for (auto s : items) {
addSignal(s->msg_id, s->sig);
}
removeIf([&](auto &s) {
return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; });
});
}
}
void ChartView::resizeEvent(QResizeEvent *event) {
qreal left, top, right, bottom;
chart()->layout()->getContentsMargins(&left, &top, &right, &bottom);
move_icon->setPos(left, top);
close_btn_proxy->setPos(rect().right() - right - close_btn_proxy->size().width(), top);
int x = close_btn_proxy->pos().x() - manage_btn_proxy->size().width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
manage_btn_proxy->setPos(x, top);
if (align_to > 0) {
updatePlotArea(align_to, true);
}
QChartView::resizeEvent(event);
}
void ChartView::updatePlotArea(int left_pos, bool force) {
if (align_to != left_pos || force) {
align_to = left_pos;
qreal left, top, right, bottom;
chart()->layout()->getContentsMargins(&left, &top, &right, &bottom);
QSizeF legend_size = chart()->legend()->layout()->minimumSize();
legend_size.setWidth(manage_btn_proxy->sceneBoundingRect().left() - move_icon->sceneBoundingRect().right());
chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), legend_size});
// add top space for signal value
int adjust_top = chart()->legend()->geometry().height() + QFontMetrics(signal_value_font).height() + 3;
adjust_top = std::max<int>(adjust_top, manage_btn_proxy->sceneBoundingRect().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin));
// add right space for x-axis label
QSizeF x_label_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, QString::number(axis_x->max(), 'f', 2));
x_label_size += QSizeF{5, 5};
chart()->setPlotArea(rect().adjusted(align_to + left, adjust_top + top, -x_label_size.width() / 2 - right, -x_label_size.height() - bottom));
chart()->layout()->invalidate();
resetChartCache();
}
}
void ChartView::updateTitle() {
for (QLegendMarker *marker : chart()->legend()->markers()) {
QObject::connect(marker, &QLegendMarker::clicked, this, &ChartView::handleMarkerClicked, Qt::UniqueConnection);
}
for (auto &s : sigs) {
auto decoration = s.series->isVisible() ? "none" : "line-through";
s.series->setName(QString("<span style=\"text-decoration:%1\"><b>%2</b> <font color=\"gray\">%3 %4</font></span>").arg(decoration, s.sig->name, msgName(s.msg_id), s.msg_id.toString()));
}
split_chart_act->setEnabled(sigs.size() > 1);
resetChartCache();
}
void ChartView::updatePlot(double cur, double min, double max) {
cur_sec = cur;
if (min != axis_x->min() || max != axis_x->max()) {
axis_x->setRange(min, max);
updateAxisY();
updateSeriesPoints();
// update tooltip
if (tooltip_x >= 0) {
showTip(chart()->mapToValue({tooltip_x, 0}).x());
}
resetChartCache();
}
viewport()->update();
}
void ChartView::updateSeriesPoints() {
// Show points when zoomed in enough
for (auto &s : sigs) {
auto begin = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan);
auto end = std::lower_bound(begin, s.vals.cend(), axis_x->max(), xLessThan);
if (begin != end) {
int num_points = std::max<int>((end - begin), 1);
QPointF right_pt = end == s.vals.cend() ? s.vals.back() : *end;
double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points;
if (series_type == SeriesType::Scatter) {
qreal size = std::clamp(pixels_per_point / 2.0, 2.0, 8.0);
if (s.series->useOpenGL()) {
size *= devicePixelRatioF();
}
((QScatterSeries *)s.series)->setMarkerSize(size);
} else {
s.series->setPointsVisible(pixels_per_point > 20);
}
}
}
}
void ChartView::updateSeries(const cabana::Signal *sig) {
for (auto &s : sigs) {
if (!sig || s.sig == sig) {
if (!can->liveStreaming()) {
s.vals.clear();
s.step_vals.clear();
s.last_value_mono_time = 0;
}
s.series->setColor(getColor(s.sig));
const auto &msgs = can->events(s.msg_id);
s.vals.reserve(msgs.capacity());
s.step_vals.reserve(msgs.capacity() * 2);
auto first = std::upper_bound(msgs.cbegin(), msgs.cend(), s.last_value_mono_time, [](uint64_t ts, auto e) {
return ts < e->mono_time;
});
const double route_start_time = can->routeStartTime();
for (auto end = msgs.cend(); first != end; ++first) {
const CanEvent *e = *first;
double value = get_raw_value(e->dat, e->size, *s.sig);
double ts = e->mono_time / 1e9 - route_start_time; // seconds
s.vals.append({ts, value});
if (!s.step_vals.empty()) {
s.step_vals.append({ts, s.step_vals.back().y()});
}
s.step_vals.append({ts, value});
s.last_value_mono_time = e->mono_time;
}
if (!can->liveStreaming()) {
s.segment_tree.build(s.vals);
}
s.series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals);
}
}
updateAxisY();
// invoke resetChartCache in ui thread
QMetaObject::invokeMethod(this, &ChartView::resetChartCache, Qt::QueuedConnection);
}
// auto zoom on yaxis
void ChartView::updateAxisY() {
if (sigs.isEmpty()) return;
double min = std::numeric_limits<double>::max();
double max = std::numeric_limits<double>::lowest();
QString unit = sigs[0].sig->unit;
for (auto &s : sigs) {
if (!s.series->isVisible()) continue;
// Only show unit when all signals have the same unit
if (unit != s.sig->unit) {
unit.clear();
}
auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan);
auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan);
s.min = std::numeric_limits<double>::max();
s.max = std::numeric_limits<double>::lowest();
if (can->liveStreaming()) {
for (auto it = first; it != last; ++it) {
if (it->y() < s.min) s.min = it->y();
if (it->y() > s.max) s.max = it->y();
}
} else {
auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
s.min = min_y;
s.max = max_y;
}
min = std::min(min, s.min);
max = std::max(max, s.max);
}
if (min == std::numeric_limits<double>::max()) min = 0;
if (max == std::numeric_limits<double>::lowest()) max = 0;
if (axis_y->titleText() != unit) {
axis_y->setTitleText(unit);
y_label_width = 0; // recalc width
}
double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05;
auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3);
if (min_y != axis_y->min() || max_y != axis_y->max() || y_label_width == 0) {
axis_y->setRange(min_y, max_y);
axis_y->setTickCount(tick_count);
int n = qMax(int(-qFloor(std::log10((max_y - min_y) / (tick_count - 1)))), 0) + 1;
int max_label_width = 0;
QFontMetrics fm(axis_y->labelsFont());
for (int i = 0; i < tick_count; i++) {
qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1));
max_label_width = std::max(max_label_width, fm.width(QString::number(value, 'f', n)));
}
int title_spacing = unit.isEmpty() ? 0 : QFontMetrics(axis_y->titleFont()).size(Qt::TextSingleLine, unit).height();
y_label_width = title_spacing + max_label_width + 15;
axis_y->setLabelFormat(QString("%.%1f").arg(n));
emit axisYLabelWidthChanged(y_label_width);
}
}
std::tuple<double, double, int> ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) {
qreal range = niceNumber((max - min), true); // range with ceiling
qreal step = niceNumber(range / (tick_count - 1), false);
min = qFloor(min / step);
max = qCeil(max / step);
tick_count = int(max - min) + 1;
return {min * step, max * step, tick_count};
}
// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n
qreal ChartView::niceNumber(qreal x, bool ceiling) {
qreal z = qPow(10, qFloor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x
qreal q = x / z; //q<10 && q>=1;
if (ceiling) {
if (q <= 1.0) q = 1;
else if (q <= 2.0) q = 2;
else if (q <= 5.0) q = 5;
else q = 10;
} else {
if (q < 1.5) q = 1;
else if (q < 3.0) q = 2;
else if (q < 7.0) q = 5;
else q = 10;
}
return q * z;
}
void ChartView::leaveEvent(QEvent *event) {
if (tip_label.isVisible()) {
charts_widget->showValueTip(-1);
}
QChartView::leaveEvent(event);
}
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
QPixmap getBlankShadowPixmap(const QPixmap &px, int radius) {
QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect;
e->setColor(QColor(40, 40, 40, 245));
e->setOffset(0, 0);
e->setBlurRadius(radius);
qreal dpr = px.devicePixelRatio();
QPixmap blank(px.size());
blank.setDevicePixelRatio(dpr);
blank.fill(Qt::white);
QGraphicsScene scene;
QGraphicsPixmapItem item(blank);
item.setGraphicsEffect(e);
scene.addItem(&item);
QPixmap shadow(px.size() + QSize(radius * dpr * 2, radius * dpr * 2));
shadow.setDevicePixelRatio(dpr);
shadow.fill(Qt::transparent);
QPainter p(&shadow);
scene.render(&p, {QPoint(), shadow.size() / dpr}, item.boundingRect().adjusted(-radius, -radius, radius, radius));
return shadow;
}
static QPixmap getDropPixmap(const QPixmap &src) {
static QPixmap shadow_px;
const int radius = 10;
if (shadow_px.size() != src.size() + QSize(radius * 2, radius * 2)) {
shadow_px = getBlankShadowPixmap(src, radius);
}
QPixmap px = shadow_px;
QPainter p(&px);
QRectF target_rect(QPointF(radius, radius), src.size() / src.devicePixelRatio());
p.drawPixmap(target_rect.topLeft(), src);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.fillRect(target_rect, QColor(0, 0, 0, 200));
return px;
}
void ChartView::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) {
QMimeData *mimeData = new QMimeData;
mimeData->setData(CHART_MIME_TYPE, QByteArray::number((qulonglong)this));
QPixmap px = grab().scaledToWidth(CHART_MIN_WIDTH * viewport()->devicePixelRatio(), Qt::SmoothTransformation);
charts_widget->stopAutoScroll();
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setPixmap(getDropPixmap(px));
drag->setHotSpot(-QPoint(5, 5));
drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction);
} else if (event->button() == Qt::LeftButton && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) {
// Save current playback state when scrubbing
resume_after_scrub = !can->isPaused();
if (resume_after_scrub) {
can->pause(true);
}
is_scrubbing = true;
} else {
QChartView::mousePressEvent(event);
}
}
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
void ChartView::mouseReleaseEvent(QMouseEvent *event) {
auto rubber = findChild<QRubberBand *>();
if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) {
rubber->hide();
QRectF rect = rubber->geometry().normalized();
double min = chart()->mapToValue(rect.topLeft()).x();
double max = chart()->mapToValue(rect.bottomRight()).x();
// Prevent zooming/seeking past the end of the route
min = std::clamp(min, 0., can->totalSeconds());
max = std::clamp(max, 0., can->totalSeconds());
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
if (rubber->width() <= 0) {
// no rubber dragged, seek to mouse position
can->seekTo(min);
} else if (rubber->width() > 10) {
charts_widget->zoom_undo_stack->push(new ZoomCommand(charts_widget, {min, max}));
} else {
viewport()->update();
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
}
event->accept();
} else if (event->button() == Qt::RightButton) {
charts_widget->zoom_undo_stack->undo();
event->accept();
} else {
QGraphicsView::mouseReleaseEvent(event);
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
}
// Resume playback if we were scrubbing
is_scrubbing = false;
if (resume_after_scrub) {
can->pause(false);
resume_after_scrub = false;
}
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
}
void ChartView::mouseMoveEvent(QMouseEvent *ev) {
const auto plot_area = chart()->plotArea();
// Scrubbing
if (is_scrubbing && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) {
if (plot_area.contains(ev->pos())) {
can->seekTo(std::clamp(chart()->mapToValue(ev->pos()).x(), 0., can->totalSeconds()));
}
}
auto rubber = findChild<QRubberBand *>();
bool is_zooming = rubber && rubber->isVisible();
clearTrackPoints();
if (!is_zooming && plot_area.contains(ev->pos())) {
const double sec = chart()->mapToValue(ev->pos()).x();
charts_widget->showValueTip(sec);
} else if (tip_label.isVisible()) {
charts_widget->showValueTip(-1);
Cabana: stable initial release (#26004) * increase form size & fix wrong charts number * set max axisy to 1.0 if no value * show 'close' button in floating window * alwasy show scroll bar * complete the logs * more * increase size to 50 * keep logs for all messages * more * rename signal * better height * avoid flicker * dont call setupdatesenabled * filter dbc files bye typing * remove all charts if dbc file changed * fix wrong idx * bolder dbc filename * update chart if signal has been edited * new signals signalAdded,signalUpdated * split class Parser into CanMessages and DBCManager * cleanup * updateState after set message * cleanup * emit msgUpdated * clear history log if selected range changed * always update time * change title layout * show selected range hide title bar if no charts less space between title and chart * custome historylogmodel for extreme fast update * move historylog to seperate file * 2 decimal * cleanup cleanup * left click on the chart to set start time * todo * show tooltip for header item&cleanup binaryview add hline to signal form * better paint * cleanup signals/slots * better range if min==max * set historylog's minheight to 300 * 3x faster,sortable message list. * zero copy in queued connection * proxymodel * clear log if loop to the begin * simplify history log * remove icon * remove assets * hide linemarker on initialization * rubber width may less than 0 * dont zoom char if selected range is too small * cleanup messageslist * don't zoom chart if selected range less than 500ms * typo * check boundary * check msg_id * capital first letter * move history log out of scrollarea * Show only one form at a time * auto scroll to header d * reduce msg size entire row clickable rename filter_msgs
3 years ago
}
QChartView::mouseMoveEvent(ev);
if (is_zooming) {
QRect rubber_rect = rubber->geometry();
rubber_rect.setLeft(std::max(rubber_rect.left(), (int)plot_area.left()));
rubber_rect.setRight(std::min(rubber_rect.right(), (int)plot_area.right()));
if (rubber_rect != rubber->geometry()) {
rubber->setGeometry(rubber_rect);
}
viewport()->update();
}
}
void ChartView::showTip(double sec) {
QRect tip_area(0, chart()->plotArea().top(), rect().width(), chart()->plotArea().height());
QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area);
if (visible_rect.isEmpty()) {
tip_label.hide();
return;
}
tooltip_x = chart()->mapToPosition({sec, 0}).x();
qreal x = tooltip_x;
QStringList text_list(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3));
for (auto &s : sigs) {
if (s.series->isVisible()) {
QString value = "--";
// use reverse iterator to find last item <= sec.
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x() > x; });
if (it != s.vals.crend() && it->x() >= axis_x->min()) {
value = QString::number(it->y());
s.track_pt = *it;
x = std::max(x, chart()->mapToPosition(*it).x());
}
QString name = sigs.size() > 1 ? s.sig->name + ": " : "";
QString min = s.min == std::numeric_limits<double>::max() ? "--" : QString::number(s.min);
QString max = s.max == std::numeric_limits<double>::lowest() ? "--" : QString::number(s.max);
text_list << QString("<span style=\"color:%1;\">■ </span>%2<b>%3</b> (%4, %5)")
.arg(s.series->color().name(), name, value, min, max);
}
}
QPoint pt(x, chart()->plotArea().top());
QString text = "<p style='white-space:pre'>" % text_list.join("<br />") % "</p>";
tip_label.showText(pt, text, this, visible_rect);
viewport()->update();
}
void ChartView::hideTip() {
clearTrackPoints();
tooltip_x = -1;
tip_label.hide();
viewport()->update();
}
void ChartView::dragEnterEvent(QDragEnterEvent *event) {
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
drawDropIndicator(event->source() != this);
event->acceptProposedAction();
}
}
void ChartView::dragMoveEvent(QDragMoveEvent *event) {
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
event->setDropAction(event->source() == this ? Qt::MoveAction : Qt::CopyAction);
event->accept();
}
charts_widget->startAutoScroll();
}
void ChartView::dropEvent(QDropEvent *event) {
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
if (event->source() != this) {
ChartView *source_chart = (ChartView *)event->source();
for (auto &s : source_chart->sigs) {
source_chart->chart()->removeSeries(s.series);
addSeries(s.series);
}
sigs.append(source_chart->sigs);
updateAxisY();
updateTitle();
startAnimation();
source_chart->sigs.clear();
charts_widget->removeChart(source_chart);
event->acceptProposedAction();
}
can_drop = false;
}
}
void ChartView::resetChartCache() {
chart_pixmap = QPixmap();
viewport()->update();
}
void ChartView::startAnimation() {
QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this);
viewport()->setGraphicsEffect(eff);
QPropertyAnimation *a = new QPropertyAnimation(eff, "opacity");
a->setDuration(250);
a->setStartValue(0.3);
a->setEndValue(1);
a->setEasingCurve(QEasingCurve::InBack);
a->start(QPropertyAnimation::DeleteWhenStopped);
}
void ChartView::paintEvent(QPaintEvent *event) {
if (!can->liveStreaming()) {
if (chart_pixmap.isNull()) {
const qreal dpr = viewport()->devicePixelRatioF();
chart_pixmap = QPixmap(viewport()->size() * dpr);
chart_pixmap.setDevicePixelRatio(dpr);
QPainter p(&chart_pixmap);
p.setRenderHints(QPainter::Antialiasing);
drawBackground(&p, viewport()->rect());
scene()->setSceneRect(viewport()->rect());
scene()->render(&p, viewport()->rect());
}
QPainter painter(viewport());
painter.setRenderHints(QPainter::Antialiasing);
painter.drawPixmap(QPoint(), chart_pixmap);
if (can_drop) {
painter.setPen(QPen(palette().color(QPalette::Highlight), 4));
painter.drawRect(viewport()->rect());
}
QRectF exposed_rect = mapToScene(event->region().boundingRect()).boundingRect();
drawForeground(&painter, exposed_rect);
} else {
QChartView::paintEvent(event);
}
}
void ChartView::drawBackground(QPainter *painter, const QRectF &rect) {
painter->fillRect(rect, palette().color(QPalette::Base));
}
void ChartView::drawForeground(QPainter *painter, const QRectF &rect) {
drawTimeline(painter);
// draw track points
painter->setPen(Qt::NoPen);
qreal track_line_x = -1;
for (auto &s : sigs) {
if (!s.track_pt.isNull() && s.series->isVisible()) {
painter->setBrush(s.series->color().darker(125));
QPointF pos = chart()->mapToPosition(s.track_pt);
painter->drawEllipse(pos, 5.5, 5.5);
track_line_x = std::max(track_line_x, pos.x());
}
}
if (track_line_x > 0) {
auto plot_area = chart()->plotArea();
painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine));
painter->drawLine(QPointF{track_line_x, plot_area.top()}, QPointF{track_line_x, plot_area.bottom()});
}
// paint points. OpenGL mode lacks certain features (such as showing points)
painter->setPen(Qt::NoPen);
for (auto &s : sigs) {
if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) {
auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan);
auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan);
painter->setBrush(s.series->color());
for (auto it = first; it != last; ++it) {
painter->drawEllipse(chart()->mapToPosition(*it), 4, 4);
}
}
}
// paint zoom range
auto rubber = findChild<QRubberBand *>();
if (rubber && rubber->isVisible() && rubber->width() > 1) {
painter->setPen(Qt::white);
auto rubber_rect = rubber->geometry().normalized();
for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) {
QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 2);
auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN);
pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2});
painter->fillRect(r, Qt::gray);
painter->drawText(r, Qt::AlignCenter, sec);
}
}
}
void ChartView::drawTimeline(QPainter *painter) {
const auto plot_area = chart()->plotArea();
// draw line
qreal x = std::clamp(chart()->mapToPosition(QPointF{cur_sec, 0}).x(), plot_area.left(), plot_area.right());
painter->setPen(QPen(chart()->titleBrush().color(), 2));
painter->drawLine(QPointF{x, plot_area.top()}, QPointF{x, plot_area.bottom() + 1});
// draw current time
QString time_str = QString::number(cur_sec, 'f', 2);
QSize time_str_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, time_str) + QSize(8, 2);
QRect time_str_rect(QPoint(x - time_str_size.width() / 2, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size);
QPainterPath path;
path.addRoundedRect(time_str_rect, 3, 3);
painter->fillPath(path, settings.theme == DARK_THEME ? Qt::darkGray : Qt::gray);
painter->setPen(palette().color(QPalette::BrightText));
painter->setFont(axis_x->labelsFont());
painter->drawText(time_str_rect, Qt::AlignCenter, time_str);
// draw signal value
auto item_group = qgraphicsitem_cast<QGraphicsItemGroup *>(chart()->legend()->childItems()[0]);
assert(item_group != nullptr);
auto legend_markers = item_group->childItems();
assert(legend_markers.size() == sigs.size());
painter->setFont(signal_value_font);
painter->setPen(chart()->legend()->labelColor());
int i = 0;
for (auto &s : sigs) {
QString value = "--";
if (s.series->isVisible()) {
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, [](auto &p, double x) { return p.x() > x; });
if (it != s.vals.crend() && it->x() >= axis_x->min()) {
value = s.sig->formatValue(it->y());
}
}
QRectF marker_rect = legend_markers[i++]->sceneBoundingRect();
QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size());
QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width());
painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val);
}
}
QXYSeries *ChartView::createSeries(SeriesType type, QColor color) {
QXYSeries *series = nullptr;
if (type == SeriesType::Line) {
series = new QLineSeries(this);
chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle);
} else if (type == SeriesType::StepLine) {
series = new QLineSeries(this);
chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries);
} else {
series = new QScatterSeries(this);
chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle);
}
series->setColor(color);
// TODO: Due to a bug in CameraWidget the camera frames
// are drawn instead of the graphs on MacOS. Re-enable OpenGL when fixed
#ifndef __APPLE__
series->setUseOpenGL(true);
// Qt doesn't properly apply device pixel ratio in OpenGL mode
QPen pen = series->pen();
pen.setWidthF(2.0 * devicePixelRatioF());
series->setPen(pen);
#endif
addSeries(series);
return series;
}
void ChartView::addSeries(QXYSeries *series) {
chart()->addSeries(series);
series->attachAxis(axis_x);
series->attachAxis(axis_y);
// disables the delivery of mouse events to the opengl widget.
// this enables the user to select the zoom area when the mouse press on the data point.
auto glwidget = findChild<QOpenGLWidget *>();
if (glwidget && !glwidget->testAttribute(Qt::WA_TransparentForMouseEvents)) {
glwidget->setAttribute(Qt::WA_TransparentForMouseEvents);
}
}
void ChartView::setSeriesType(SeriesType type) {
if (type != series_type) {
series_type = type;
for (auto &s : sigs) {
chart()->removeSeries(s.series);
s.series->deleteLater();
}
for (auto &s : sigs) {
auto series = createSeries(series_type, getColor(s.sig));
series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals);
s.series = series;
}
updateSeriesPoints();
updateTitle();
}
}
void ChartView::handleMarkerClicked() {
auto marker = qobject_cast<QLegendMarker *>(sender());
Q_ASSERT(marker);
if (sigs.size() > 1) {
auto series = marker->series();
series->setVisible(!series->isVisible());
marker->setVisible(true);
updateAxisY();
updateTitle();
}
}