autonomy_datasets v1.6.0
Loading...
Searching...
No Matches
playback_panel.cpp
Go to the documentation of this file.
1// Copyright Thinking Cars GmbH
2// SPDX-License-Identifier: Apache-2.0
3
5
6#include <memory>
7
8#include <QFontMetrics>
9#include <QFormLayout>
10#include <QFrame>
11#include <QHBoxLayout>
12#include <QRegularExpression>
13#include <QSizePolicy>
14#include <QStringList>
15#include <QVBoxLayout>
16
17#include "pluginlib/class_list_macros.hpp"
18#include "rviz_common/display_context.hpp"
19
21
22namespace {
23
24using RequestSamples = autonomy_datasets_msgs::srv::RequestSamples;
25
27const char* const kDefaultServiceName = "/datasets/request_samples";
28
30constexpr int kAvailabilityPollIntervalMs = 500;
31
33constexpr int kMaximumStepSize = 100000;
34
35} // namespace
36
37PlaybackPanel::PlaybackPanel(QWidget* parent) : rviz_common::Panel(parent) { setupUi(); }
38
40 // Drops the pending request together with its callback, which would otherwise report into a
41 // panel that no longer exists. RViz spins its node from the GUI thread that also destroys the
42 // panel, so no response can be in the middle of being delivered while this runs.
43 client_.reset();
44}
45
47 // NOLINTBEGIN(cppcoreguidelines-owning-memory) Qt widgets are owned by their parent widget
48 service_name_edit_ = new QLineEdit(QString::fromUtf8(kDefaultServiceName), this);
49 service_name_edit_->setToolTip(
50 tr("Name of the 'request_samples' service of the dataset node, as reported by 'ros2 service list'"));
51
52 auto* service_layout = new QFormLayout();
53 service_layout->addRow(tr("Service"), service_name_edit_);
54
55 pause_button_ = new QPushButton(tr("Pause"), this);
56 pause_button_->setToolTip(tr("Stop the playback at the current sample, without publishing another one"));
57 play_button_ = new QPushButton(tr("Play to End"), this);
58 play_button_->setToolTip(
59 tr("Publish all remaining samples of the dataset.\n"
60 "The dataset node answers this request only once the dataset ends, so the playback cannot be paused again "
61 "until then."));
62
63 auto* transport_layout = new QHBoxLayout();
64 transport_layout->addWidget(pause_button_);
65 transport_layout->addWidget(play_button_);
66
67 step_size_spin_ = new QSpinBox(this);
68 step_size_spin_->setRange(1, kMaximumStepSize);
69 step_size_spin_->setValue(1);
70 step_size_spin_->setToolTip(tr("Number of samples a step publishes"));
71 step_button_ = new QPushButton(tr("Step"), this);
72 step_button_->setToolTip(tr("Publish the next samples, whichever they are"));
73
74 auto* step_layout = new QHBoxLayout();
75 step_layout->addWidget(step_size_spin_);
76 step_layout->addWidget(new QLabel(tr("sample(s)"), this));
77 step_layout->addStretch();
78 step_layout->addWidget(step_button_);
79
80 sample_ids_edit_ = new QLineEdit(this);
81 sample_ids_edit_->setPlaceholderText(tr("e.g. 120 or 10, 20, 30"));
82 sample_ids_edit_->setToolTip(
83 tr("IDs of the samples to publish, skipping every sample in between.\n"
84 "The playback is forward-only, so it cannot go back to a sample it has already passed."));
85 skip_button_ = new QPushButton(tr("Skip To"), this);
86 skip_button_->setToolTip(tr("Skip ahead to the entered samples"));
87
88 auto* skip_layout = new QHBoxLayout();
89 skip_layout->addWidget(sample_ids_edit_);
90 skip_layout->addWidget(skip_button_);
91
92 auto* separator = new QFrame(this);
93 separator->setFrameShape(QFrame::HLine);
94 separator->setFrameShadow(QFrame::Sunken);
95
96 position_label_ = new QLabel(this);
97 status_label_ = new QLabel(this);
98 for (QLabel* label : {position_label_, status_label_}) {
99 // The layout gives the labels whatever width the controls above them need, instead of the
100 // labels asking for the width their text would take
101 label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
102 label->installEventFilter(this);
103 }
104
105 auto* layout = new QVBoxLayout();
106 layout->addLayout(service_layout);
107 layout->addLayout(transport_layout);
108 layout->addLayout(step_layout);
109 layout->addLayout(skip_layout);
110 layout->addWidget(separator);
111 layout->addWidget(position_label_);
112 layout->addWidget(status_label_);
113 layout->addStretch();
114 setLayout(layout);
115
116 availability_timer_ = new QTimer(this);
117 // NOLINTEND(cppcoreguidelines-owning-memory)
118
119 connect(service_name_edit_, &QLineEdit::editingFinished, this, &PlaybackPanel::onServiceNameChanged);
120 connect(pause_button_, &QPushButton::clicked, this, &PlaybackPanel::onPauseClicked);
121 connect(play_button_, &QPushButton::clicked, this, &PlaybackPanel::onPlayClicked);
122 connect(step_button_, &QPushButton::clicked, this, &PlaybackPanel::onStepClicked);
123 connect(skip_button_, &QPushButton::clicked, this, &PlaybackPanel::onSkipClicked);
124 connect(sample_ids_edit_, &QLineEdit::returnPressed, this, &PlaybackPanel::onSkipClicked);
125 connect(availability_timer_, &QTimer::timeout, this, &PlaybackPanel::updateAvailability);
126 connect(step_size_spin_, QOverload<int>::of(&QSpinBox::valueChanged), this, &PlaybackPanel::configChanged);
127
128 // The response of a request may reach the panel on whichever thread the executor of RViz runs
129 // on, so the widgets are updated through a queued connection instead of from the callback.
130 connect(this, &PlaybackPanel::requestFinished, this, &PlaybackPanel::applyRequestResult, Qt::QueuedConnection);
131
132 showPosition(tr("No sample published yet"));
133 showStatus(tr("Not connected to a dataset node yet"));
135}
136
137bool PlaybackPanel::eventFilter(QObject* watched, QEvent* event) {
138 if (event->type() == QEvent::Resize) {
139 if (watched == position_label_) {
141 } else if (watched == status_label_) {
143 }
144 }
145 return rviz_common::Panel::eventFilter(watched, event);
146}
147
148void PlaybackPanel::showPosition(const QString& text) {
149 position_text_ = text;
151}
152
153void PlaybackPanel::showStatus(const QString& text) {
154 status_text_ = text;
156}
157
158void PlaybackPanel::showElided(QLabel* label, const QString& text) {
159 const QString shown = label->fontMetrics().elidedText(text, Qt::ElideRight, label->contentsRect().width());
160 if (shown != label->text()) {
161 // Guards against a resize that the new text triggers in turn
162 label->setText(shown);
163 }
164 // Marked up as rich text, so that a long text is wrapped over several lines of the tooltip
165 label->setToolTip(text.isEmpty() ? QString() : QString("<qt>%1</qt>").arg(text.toHtmlEscaped()));
166}
167
169 node_ = getDisplayContext()->getRosNodeAbstraction().lock()->get_raw_node();
170 createClient();
171 availability_timer_->start(kAvailabilityPollIntervalMs);
172}
173
174void PlaybackPanel::load(const rviz_common::Config& config) {
175 rviz_common::Panel::load(config);
176
177 QString service_name;
178 if (config.mapGetString("ServiceName", &service_name) && !service_name.isEmpty()) {
179 service_name_edit_->setText(service_name);
180 }
181 float step_size = 0.0F;
182 if (config.mapGetFloat("StepSize", &step_size) && step_size >= 1.0F) {
183 step_size_spin_->setValue(static_cast<int>(step_size));
184 }
185 if (node_ != nullptr) {
186 createClient();
187 }
188}
189
190void PlaybackPanel::save(rviz_common::Config config) const {
191 rviz_common::Panel::save(config);
192 config.mapSetValue("ServiceName", service_name_edit_->text());
193 config.mapSetValue("StepSize", step_size_spin_->value());
194}
195
197 const QString service_name = service_name_edit_->text().trimmed();
198 if (node_ == nullptr || service_name.isEmpty()) {
199 client_.reset();
201 return;
202 }
203 client_ = node_->create_client<RequestSamples>(service_name.toStdString());
204 service_available_ = false;
206}
207
209 createClient();
210 Q_EMIT configChanged();
211}
212
214 const bool available = client_ != nullptr && client_->service_is_ready();
215 if (available == service_available_) {
217 return;
218 }
219 service_available_ = available;
220
221 if (!available && request_in_flight_) {
222 // The node answering the request is gone, so its response will never arrive
223 request_in_flight_ = false;
224 showStatus(tr("The dataset node disappeared while it was processing the request"));
225 } else if (!available) {
226 showStatus(tr("Waiting for service '%1'").arg(service_name_edit_->text().trimmed()));
227 } else if (!request_in_flight_) {
228 showStatus(tr("Connected to '%1'").arg(service_name_edit_->text().trimmed()));
229 }
231}
232
234 const bool ready = service_available_ && !request_in_flight_;
235 pause_button_->setEnabled(ready);
236 play_button_->setEnabled(ready);
237 step_button_->setEnabled(ready);
238 skip_button_->setEnabled(ready);
239 step_size_spin_->setEnabled(ready);
240 sample_ids_edit_->setEnabled(ready);
241}
242
244 // A request for no sample at all takes control of the playback without publishing anything,
245 // which leaves the playback waiting for the next request
246 sendRequest(RequestSamples::Request::MODE_NEXT_SAMPLES, 0, {}, tr("Pausing playback..."));
247}
248
250 const auto num_samples = static_cast<uint64_t>(step_size_spin_->value());
251 sendRequest(RequestSamples::Request::MODE_NEXT_SAMPLES, num_samples, {},
252 tr("Publishing the next %1 sample(s)...").arg(static_cast<qulonglong>(num_samples)));
253}
254
256 QString error;
257 const std::vector<uint64_t> sample_ids = parseSampleIds(&error);
258 if (!error.isEmpty()) {
259 showStatus(error);
260 return;
261 }
262 if (sample_ids.empty()) {
263 showStatus(tr("Enter the ID of the sample to skip to"));
264 return;
265 }
266 sendRequest(RequestSamples::Request::MODE_SAMPLE_IDS, 0, sample_ids,
267 tr("Skipping ahead to %1 sample(s)...").arg(static_cast<qulonglong>(sample_ids.size())));
268}
269
271 sendRequest(RequestSamples::Request::MODE_ALL_SAMPLES, 0, {}, tr("Publishing all remaining samples..."));
272}
273
274std::vector<uint64_t> PlaybackPanel::parseSampleIds(QString* error) const {
275 std::vector<uint64_t> sample_ids;
276 const QStringList entries = sample_ids_edit_->text().split(QRegularExpression("[,;\\s]+"), Qt::SkipEmptyParts);
277 for (const QString& entry : entries) {
278 bool valid = false;
279 const qulonglong sample_id = entry.toULongLong(&valid);
280 if (!valid) {
281 *error = tr("'%1' is not a sample ID").arg(entry);
282 return {};
283 }
284 sample_ids.push_back(static_cast<uint64_t>(sample_id));
285 }
286 return sample_ids;
287}
288
290 uint64_t num_samples,
291 const std::vector<uint64_t>& sample_ids,
292 const QString& pending_status) {
293 if (client_ == nullptr || !client_->service_is_ready()) {
294 showStatus(tr("Service '%1' is not available").arg(service_name_edit_->text().trimmed()));
295 return;
296 }
297
298 auto request = std::make_shared<RequestSamples::Request>();
299 request->mode = mode;
300 request->num_samples = num_samples;
301 request->sample_ids = sample_ids;
302
303 request_in_flight_ = true;
304 showStatus(pending_status);
306
307 client_->async_send_request(request, [this](rclcpp::Client<RequestSamples>::SharedFuture future) {
308 const auto response = future.get();
309 QString status = QString::fromStdString(response->message);
310 if (response->end_of_dataset) {
311 status += tr("; the dataset has ended");
312 }
313 Q_EMIT requestFinished(describePosition(*response), status, response->success);
314 });
315}
316
317QString PlaybackPanel::describePosition(const RequestSamples::Response& response) {
318 if (response.published_sample_ids.empty()) {
319 return {};
320 }
321 const uint64_t sample_id = response.published_sample_ids.back();
322 if (response.published_scene_ids.size() != response.published_sample_ids.size()) {
323 return tr("Sample %1").arg(static_cast<qulonglong>(sample_id));
324 }
325 return tr("Sample %1 of scene '%2'")
326 .arg(static_cast<qulonglong>(sample_id))
327 .arg(QString::fromStdString(response.published_scene_ids.back()));
328}
329
330void PlaybackPanel::applyRequestResult(const QString& position, const QString& status, bool success) {
331 request_in_flight_ = false;
332 if (!position.isEmpty()) {
333 showPosition(position);
334 }
335 showStatus(success ? tr("OK - %1").arg(status) : tr("Failed - %1").arg(status));
337}
338
339} // namespace autonomy_datasets_rviz_plugins
340
341PLUGINLIB_EXPORT_CLASS(autonomy_datasets_rviz_plugins::PlaybackPanel, rviz_common::Panel)
342
343// Compiles the code Qt generates for the signals and slots of the panel into this translation
344// unit, instead of leaving it in one of its own that would have to be excluded from linting.
345#include "autonomy_datasets_rviz_plugins/moc_playback_panel.cpp"
void onServiceNameChanged()
Re-creates the service client after the service name has been edited.
rclcpp::Node::SharedPtr node_
ROS node of RViz, which its executor spins.
void onStepClicked()
Requests the next samples, which advances the playback by the configured step size.
QPushButton * play_button_
Runs the playback to the end of the dataset.
QString status_text_
Full text reported by status_label_, of which only what fits is shown.
QString position_text_
Full text reported by position_label_, of which only what fits is shown.
QPushButton * step_button_
Advances the playback by the configured step size.
void showStatus(const QString &text)
Reports what the panel is waiting for or what a request resulted in.
QPushButton * skip_button_
Skips the playback ahead to the entered sample IDs.
static QString describePosition(const autonomy_datasets_msgs::srv::RequestSamples::Response &response)
Describes the sample a response reports as published last.
void save(rviz_common::Config config) const override
Stores the service name and step size in the RViz configuration.
void onSkipClicked()
Requests the entered sample IDs, which skips the playback ahead to them.
QLabel * position_label_
Sample the dataset node published last.
~PlaybackPanel() override
Destroys the panel, dropping the service client before the RViz node goes away.
void onInitialize() override
Picks up the ROS node of RViz and creates the client for the configured service.
void updateAvailability()
Tracks whether the service is available and reflects it in the panel.
QLineEdit * sample_ids_edit_
Sample IDs to skip ahead to.
void onPauseClicked()
Requests no sample at all, which stops the playback at the current sample.
bool service_available_
Whether the service was available the last time it was polled.
bool eventFilter(QObject *watched, QEvent *event) override
Re-shortens the reported texts whenever one of the labels showing them is resized.
static void showElided(QLabel *label, const QString &text)
Shows as much of a text as fits into a label and offers all of it as its tooltip.
void setupUi()
Creates the widgets of the panel and connects them to their handlers.
rclcpp::Client< autonomy_datasets_msgs::srv::RequestSamples >::SharedPtr client_
Client of the request_samples service of the dataset node.
void onPlayClicked()
Requests all remaining samples, which runs the playback to the end of the dataset.
std::vector< uint64_t > parseSampleIds(QString *error) const
Reads the sample IDs entered in the panel.
void requestFinished(const QString &position, const QString &status, bool success)
Reports the outcome of a sample request from the thread that received the response.
void load(const rviz_common::Config &config) override
Restores the service name and step size stored in the RViz configuration.
bool request_in_flight_
Whether a request has been sent whose response has not arrived yet.
QPushButton * pause_button_
Stops the playback at the current sample.
void applyRequestResult(const QString &position, const QString &status, bool success)
Shows the outcome of a sample request and re-enables the controls.
PlaybackPanel(QWidget *parent=nullptr)
Creates the panel and its widgets, without connecting to a service yet.
QLineEdit * service_name_edit_
Name of the request_samples service of the dataset node to control.
void updateControlState()
Enables the controls that may be used while no request is being processed.
QTimer * availability_timer_
Polls whether the service is available, as the panel cannot be notified about it.
void sendRequest(uint8_t mode, uint64_t num_samples, const std::vector< uint64_t > &sample_ids, const QString &pending_status)
Sends a sample request and reports its response through requestFinished().
void createClient()
Creates the service client for the service name currently entered in the panel.
void showPosition(const QString &text)
Reports which sample the dataset node published last.
QSpinBox * step_size_spin_
Number of samples a step advances the playback by.
QLabel * status_label_
Outcome of the last request, or why no request can be sent.