#include "path_tracker.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @brief AGV Path Visualization Widget * Displays the reference path and AGV trajectory */ class PathVisualizationWidget : public QWidget { Q_OBJECT public: explicit PathVisualizationWidget(QWidget* parent = nullptr) : QWidget(parent), current_step_(0), show_animation_(false) { setMinimumSize(600, 600); setStyleSheet("background-color: white;"); } void setPath(const PathCurve& path) { path_ = path; update(); } void setControlSequence(const ControlSequence& sequence) { sequence_ = sequence; current_step_ = 0; update(); } void setCurrentStep(int step) { current_step_ = step; update(); } void setShowAnimation(bool show) { show_animation_ = show; update(); } protected: void paintEvent(QPaintEvent* event) override { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // Calculate coordinate transformation const auto& path_points = path_.getPathPoints(); if (path_points.empty()) return; // Find bounds double min_x = 1e9, max_x = -1e9, min_y = 1e9, max_y = -1e9; for (const auto& pt : path_points) { min_x = std::min(min_x, pt.x); max_x = std::max(max_x, pt.x); min_y = std::min(min_y, pt.y); max_y = std::max(max_y, pt.y); } // Add trajectory points if (!sequence_.predicted_states.empty()) { for (const auto& state : sequence_.predicted_states) { min_x = std::min(min_x, state.x); max_x = std::max(max_x, state.x); min_y = std::min(min_y, state.y); max_y = std::max(max_y, state.y); } } // Add margin double margin = 0.5; min_x -= margin; max_x += margin; min_y -= margin; max_y += margin; double range_x = max_x - min_x; double range_y = max_y - min_y; double range = std::max(range_x, range_y); // Center the view double center_x = (min_x + max_x) / 2.0; double center_y = (min_y + max_y) / 2.0; // Scale to fit widget with padding int padding = 40; // Prevent division by zero if all points are at the same location if (range < 1e-6) { range = 1.0; } double scale = std::min(width() - 2 * padding, height() - 2 * padding) / range; // Coordinate transformation: world to screen auto toScreen = [&](double x, double y) -> QPointF { double sx = (x - center_x) * scale + width() / 2.0; double sy = height() / 2.0 - (y - center_y) * scale; // Flip Y axis return QPointF(sx, sy); }; // Draw grid painter.setPen(QPen(QColor(220, 220, 220), 1)); int grid_lines = 10; for (int i = 0; i <= grid_lines; ++i) { double t = static_cast(i) / grid_lines; double x = min_x + t * range; double y = min_y + t * range; QPointF p1 = toScreen(x, min_y); QPointF p2 = toScreen(x, min_y + range); painter.drawLine(p1, p2); p1 = toScreen(min_x, y); p2 = toScreen(min_x + range, y); painter.drawLine(p1, p2); } // Draw axes painter.setPen(QPen(Qt::black, 2)); QPointF origin = toScreen(0, 0); QPointF x_axis = toScreen(1, 0); QPointF y_axis = toScreen(0, 1); painter.drawLine(origin, x_axis); painter.drawLine(origin, y_axis); painter.drawText(x_axis + QPointF(5, 5), "X"); painter.drawText(y_axis + QPointF(5, 5), "Y"); // Draw reference path painter.setPen(QPen(QColor(100, 100, 255), 3, Qt::DashLine)); for (size_t i = 1; i < path_points.size(); ++i) { QPointF p1 = toScreen(path_points[i-1].x, path_points[i-1].y); QPointF p2 = toScreen(path_points[i].x, path_points[i].y); painter.drawLine(p1, p2); } // Draw path points painter.setPen(Qt::NoPen); painter.setBrush(QColor(100, 100, 255, 100)); for (const auto& pt : path_points) { QPointF p = toScreen(pt.x, pt.y); painter.drawEllipse(p, 3, 3); } // Draw predicted trajectory if (!sequence_.predicted_states.empty()) { painter.setPen(QPen(QColor(255, 100, 100), 2)); for (size_t i = 1; i < sequence_.predicted_states.size(); ++i) { QPointF p1 = toScreen(sequence_.predicted_states[i-1].x, sequence_.predicted_states[i-1].y); QPointF p2 = toScreen(sequence_.predicted_states[i].x, sequence_.predicted_states[i].y); painter.drawLine(p1, p2); } // Draw current AGV position if (show_animation_ && current_step_ < sequence_.predicted_states.size()) { const auto& state = sequence_.predicted_states[current_step_]; QPointF pos = toScreen(state.x, state.y); // Draw AGV body (rectangle) painter.save(); painter.translate(pos); painter.rotate(-state.theta * 180.0 / M_PI); // Rotate to heading double agv_length = 0.3 * scale; double agv_width = 0.2 * scale; painter.setBrush(QColor(50, 200, 50)); painter.setPen(QPen(Qt::black, 2)); painter.drawRect(QRectF(-agv_length/2, -agv_width/2, agv_length, agv_width)); // Draw heading indicator (arrow) painter.setBrush(QColor(255, 50, 50)); painter.drawEllipse(QPointF(agv_length/2, 0), agv_width/4, agv_width/4); painter.restore(); // Draw position label painter.setPen(Qt::black); painter.drawText(pos + QPointF(10, -10), QString("Step: %1").arg(current_step_)); } } // Draw legend int legend_x = 10; int legend_y = 10; painter.fillRect(legend_x, legend_y, 150, 80, QColor(255, 255, 255, 200)); painter.setPen(Qt::black); painter.drawRect(legend_x, legend_y, 150, 80); // Reference path painter.setPen(QPen(QColor(100, 100, 255), 3, Qt::DashLine)); painter.drawLine(legend_x + 10, legend_y + 20, legend_x + 40, legend_y + 20); painter.setPen(Qt::black); painter.drawText(legend_x + 50, legend_y + 25, "Reference Path"); // Trajectory painter.setPen(QPen(QColor(255, 100, 100), 3)); painter.drawLine(legend_x + 10, legend_y + 40, legend_x + 40, legend_y + 40); painter.setPen(Qt::black); painter.drawText(legend_x + 50, legend_y + 45, "Trajectory"); // AGV painter.fillRect(legend_x + 10, legend_y + 55, 30, 15, QColor(50, 200, 50)); painter.drawRect(legend_x + 10, legend_y + 55, 30, 15); painter.drawText(legend_x + 50, legend_y + 65, "AGV"); } private: PathCurve path_; ControlSequence sequence_; int current_step_; bool show_animation_; }; /** * @brief Main Window for AGV Path Tracking GUI */ class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget* parent = nullptr) : QMainWindow(parent) { setWindowTitle("AGV Path Tracking Control System - Qt GUI"); resize(1200, 800); // Create central widget QWidget* central = new QWidget(this); setCentralWidget(central); QHBoxLayout* main_layout = new QHBoxLayout(central); // Left panel: Visualization visualization_ = new PathVisualizationWidget(this); main_layout->addWidget(visualization_, 2); // Right panel: Controls and data QWidget* right_panel = new QWidget(this); QVBoxLayout* right_layout = new QVBoxLayout(right_panel); // AGV Parameters Group QGroupBox* param_group = new QGroupBox("AGV Parameters", this); QVBoxLayout* param_layout = new QVBoxLayout(param_group); wheelbase_spin_ = createParamRow("Wheelbase (m):", 0.5, 3.0, 1.0, param_layout); max_vel_spin_ = createParamRow("Max Velocity (m/s):", 0.5, 5.0, 2.0, param_layout); max_steer_spin_ = createParamRow("Max Steering (deg):", 10, 60, 45, param_layout); right_layout->addWidget(param_group); // Control Parameters Group QGroupBox* control_group = new QGroupBox("Control Parameters", this); QVBoxLayout* control_layout = new QVBoxLayout(control_group); // Algorithm selection QHBoxLayout* algo_layout = new QHBoxLayout(); algo_layout->addWidget(new QLabel("Algorithm:", this)); algorithm_combo_ = new QComboBox(this); algorithm_combo_->addItem("Pure Pursuit"); algorithm_combo_->addItem("Stanley"); algo_layout->addWidget(algorithm_combo_); control_layout->addLayout(algo_layout); // Path type selection QHBoxLayout* path_layout = new QHBoxLayout(); path_layout->addWidget(new QLabel("Path Type:", this)); path_combo_ = new QComboBox(this); path_combo_->addItem("Circle Arc"); path_combo_->addItem("Straight Line"); path_combo_->addItem("S-Curve"); //自定义路径 path_combo_->addItem("Load from CSV"); path_combo_->addItem("Custom Spline"); path_layout->addWidget(path_combo_); control_layout->addLayout(path_layout); dt_spin_ = createParamRow("Time Step (s):", 0.01, 1.0, 0.1, control_layout); horizon_spin_ = createParamRow("Horizon (s):", 1.0, 100.0, 50.0, control_layout); right_layout->addWidget(control_group); control_layout->addLayout(path_layout); // 添加自定义路径按钮 QHBoxLayout* custom_btn_layout = new QHBoxLayout(); QPushButton* load_csv_btn = new QPushButton("Browse CSV...", this); connect(load_csv_btn, &QPushButton::clicked, [this]() { QString filename = QFileDialog::getOpenFileName( this, "Open CSV Path File", "", "CSV Files (*.csv)"); if (!filename.isEmpty()) { // 修复: 使用toLocal8Bit以正确处理Windows路径(包括中文路径) if (custom_path_.loadFromCSV(filename.toLocal8Bit().constData(), true)) { custom_path_loaded_ = true; QMessageBox::information(this, "Success", QString("Loaded %1 points from CSV!").arg( custom_path_.getPathPoints().size())); } else { QMessageBox::warning(this, "Error", "Failed to load CSV file!"); } } }); custom_btn_layout->addWidget(load_csv_btn); QPushButton* save_csv_btn = new QPushButton("Save Path...", this); connect(save_csv_btn, &QPushButton::clicked, [this]() { QString filename = QFileDialog::getSaveFileName( this, "Save Path as CSV", "my_path.csv", "CSV Files (*.csv)"); // 修复: 使用toLocal8Bit以正确处理Windows路径(包括中文路径) if (!filename.isEmpty() && custom_path_loaded_) { if (custom_path_.saveToCSV(filename.toLocal8Bit().constData())) { QMessageBox::information(this, "Success", "Path saved!"); } } }); custom_btn_layout->addWidget(save_csv_btn); control_layout->addLayout(custom_btn_layout); // Buttons QHBoxLayout* button_layout = new QHBoxLayout(); QPushButton* generate_btn = new QPushButton("Generate Control", this); connect(generate_btn, &QPushButton::clicked, this, &MainWindow::generateControl); button_layout->addWidget(generate_btn); start_btn_ = new QPushButton("Start Animation", this); connect(start_btn_, &QPushButton::clicked, this, &MainWindow::toggleAnimation); start_btn_->setEnabled(false); button_layout->addWidget(start_btn_); right_layout->addLayout(button_layout); // Statistics Group stats_group_ = new QGroupBox("Statistics", this); QVBoxLayout* stats_layout = new QVBoxLayout(stats_group_); stats_label_ = new QLabel("No data", this); stats_label_->setWordWrap(true); stats_layout->addWidget(stats_label_); right_layout->addWidget(stats_group_); // Control Sequence Table table_ = new QTableWidget(this); table_->setColumnCount(4); table_->setHorizontalHeaderLabels({"Step", "Time(s)", "Velocity(m/s)", "Steering(deg)"}); table_->horizontalHeader()->setStretchLastSection(true); table_->setAlternatingRowColors(true); right_layout->addWidget(table_); main_layout->addWidget(right_panel, 1); // Initialize AGV model updateAGVModel(); // Animation timer animation_timer_ = new QTimer(this); connect(animation_timer_, &QTimer::timeout, this, &MainWindow::updateAnimation); } private slots: void generateControl() { // Update AGV model updateAGVModel(); // Create path based on selection PathCurve path; QString path_type = path_combo_->currentText(); if (path_type == "Load from CSV") { if (!custom_path_loaded_) { QMessageBox::warning(this, "Warning", "Please load a CSV file first using 'Browse CSV...' button!"); return; } path = custom_path_; } else if (path_type == "Custom Spline") { if (!custom_path_loaded_) { // 如果没有预加载,让用户输入关键点 bool ok; int num_points = QInputDialog::getInt(this, "Spline Input", "Number of key points (2-10):", 4, 2, 10, 1, &ok); if (!ok) return; std::vector key_points; for (int i = 0; i < num_points; ++i) { double x = QInputDialog::getDouble(this, "Key Point", QString("Point %1 - X coordinate:").arg(i+1), i * 3.0, -100, 100, 2, &ok); if (!ok) return; double y = QInputDialog::getDouble(this, "Key Point", QString("Point %1 - Y coordinate:").arg(i+1), (i % 2) * 3.0, -100, 100, 2, &ok); if (!ok) return; key_points.push_back(PathPoint(x, y)); } path.generateSpline(key_points, 200, 0.5); custom_path_ = path; custom_path_loaded_ = true; } else { path = custom_path_; } } else if (path_type == "Circle Arc") { path.generateCircleArc(5.0, 0.0, 5.0, M_PI, M_PI / 2, 100); } else if (path_type == "Straight Line") { PathPoint start(0, 0, 0, 0); PathPoint end(10, 0, 0, 0); path.generateLine(start, end, 100); } else if (path_type == "S-Curve") { PathPoint p0(0, 0, 0, 0); PathPoint p1(3, 2, 0, 0); PathPoint p2(7, 2, 0, 0); PathPoint p3(10, 0, 0, 0); path.generateCubicBezier(p0, p1, p2, p3, 100); } // 验证路径 if (path.getPathPoints().empty()) { QMessageBox::warning(this, "Error", "Invalid path!"); return; } // Set up tracker tracker_->setReferencePath(path); // 修复: 从路径起点获取初始状态,确保完美匹配 const auto& path_points = path.getPathPoints(); AGVModel::State initial_state; if (!path_points.empty()) { const PathPoint& start = path_points[0]; initial_state = AGVModel::State(start.x, start.y, start.theta); } else { initial_state = AGVModel::State(0.0, 0.0, 0.0); } tracker_->setInitialState(initial_state); // Generate control sequence QString algo = algorithm_combo_->currentText(); std::string algo_str = (algo == "Pure Pursuit") ? "pure_pursuit" : "stanley"; double dt = dt_spin_->value(); double horizon = horizon_spin_->value(); // 修复: 使用GUI中的速度参数 double desired_velocity = max_vel_spin_->value(); tracker_->generateControlSequence(algo_str, dt, horizon, desired_velocity); const ControlSequence& sequence = tracker_->getControlSequence(); // Update visualization visualization_->setPath(path); visualization_->setControlSequence(sequence); visualization_->setCurrentStep(0); visualization_->setShowAnimation(true); // Update table updateTable(sequence); // Update statistics updateStatistics(sequence); // Enable animation button start_btn_->setEnabled(true); start_btn_->setText("Start Animation"); animation_running_ = false; } void toggleAnimation() { const ControlSequence& sequence = tracker_->getControlSequence(); if (sequence.size() == 0) return; if (animation_running_) { // Pause the animation animation_timer_->stop(); start_btn_->setText("Resume Animation"); animation_running_ = false; } else { // Resume or start animation // Only reset to beginning if animation has finished if (animation_step_ >= sequence.size()) { animation_step_ = 0; } animation_timer_->start(100); // 100ms interval start_btn_->setText("Pause Animation"); animation_running_ = true; } } void updateAnimation() { const ControlSequence& sequence = tracker_->getControlSequence(); if (animation_step_ >= sequence.size()) { animation_timer_->stop(); start_btn_->setText("Restart Animation"); animation_running_ = false; return; } visualization_->setCurrentStep(animation_step_); // Highlight current row in table table_->selectRow(animation_step_); animation_step_++; } private: void updateAGVModel() { double wheelbase = wheelbase_spin_->value(); double max_vel = max_vel_spin_->value(); double max_steer = max_steer_spin_->value() * M_PI / 180.0; model_ = std::make_unique(wheelbase, max_vel, max_steer); tracker_ = std::make_unique(*model_); } QDoubleSpinBox* createParamRow(const QString& label, double min, double max, double value, QVBoxLayout* layout) { QHBoxLayout* row = new QHBoxLayout(); row->addWidget(new QLabel(label, this)); QDoubleSpinBox* spin = new QDoubleSpinBox(this); spin->setRange(min, max); spin->setValue(value); spin->setSingleStep((max - min) / 100.0); spin->setDecimals(2); row->addWidget(spin); layout->addLayout(row); return spin; } void updateTable(const ControlSequence& sequence) { table_->setRowCount(sequence.size()); for (size_t i = 0; i < sequence.size(); ++i) { table_->setItem(i, 0, new QTableWidgetItem(QString::number(i))); table_->setItem(i, 1, new QTableWidgetItem( QString::number(sequence.timestamps[i], 'f', 2))); table_->setItem(i, 2, new QTableWidgetItem( QString::number(sequence.controls[i].v, 'f', 4))); table_->setItem(i, 3, new QTableWidgetItem( QString::number(sequence.controls[i].delta * 180.0 / M_PI, 'f', 2))); } } void updateStatistics(const ControlSequence& sequence) { if (sequence.size() == 0) { stats_label_->setText("No data"); return; } double avg_vel = 0, max_vel = -1e9, min_vel = 1e9; double avg_steer = 0, max_steer = -1e9, min_steer = 1e9; for (const auto& ctrl : sequence.controls) { avg_vel += ctrl.v; max_vel = std::max(max_vel, ctrl.v); min_vel = std::min(min_vel, ctrl.v); double delta_deg = ctrl.delta * 180.0 / M_PI; avg_steer += delta_deg; max_steer = std::max(max_steer, delta_deg); min_steer = std::min(min_steer, delta_deg); } avg_vel /= sequence.size(); avg_steer /= sequence.size(); QString stats = QString( "Total Steps: %1\n\n" "Velocity:\n" " Avg: %2 m/s\n" " Max: %3 m/s\n" " Min: %4 m/s\n\n" "Steering:\n" " Avg: %5°\n" " Max: %6°\n" " Min: %7°" ).arg(sequence.size()) .arg(avg_vel, 0, 'f', 4) .arg(max_vel, 0, 'f', 4) .arg(min_vel, 0, 'f', 4) .arg(avg_steer, 0, 'f', 2) .arg(max_steer, 0, 'f', 2) .arg(min_steer, 0, 'f', 2); stats_label_->setText(stats); } // Widgets PathVisualizationWidget* visualization_; QTableWidget* table_; QComboBox* algorithm_combo_; QComboBox* path_combo_; QDoubleSpinBox* wheelbase_spin_; QDoubleSpinBox* max_vel_spin_; QDoubleSpinBox* max_steer_spin_; QDoubleSpinBox* dt_spin_; QDoubleSpinBox* horizon_spin_; QPushButton* start_btn_; QGroupBox* stats_group_; QLabel* stats_label_; // Model and tracker std::unique_ptr model_; std::unique_ptr tracker_; // Animation QTimer* animation_timer_; int animation_step_; bool animation_running_ = false; //自定义路径 PathCurve custom_path_; bool custom_path_loaded_ = false; }; int main(int argc, char* argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); } #include "qt_gui_demo.moc"