00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065 import os
00066 import math
00067 import codecs
00068 import threading
00069 import rospkg
00070 from rqt_bag import MessageView
00071
00072 from python_qt_binding import loadUi
00073 from python_qt_binding.QtCore import Qt, qWarning, Signal
00074 from python_qt_binding.QtGui import QDoubleValidator, QIcon
00075 from python_qt_binding.QtWidgets import QWidget, QPushButton, QTreeWidget, QTreeWidgetItem, QSizePolicy
00076
00077 from rqt_plot.data_plot import DataPlot
00078
00079
00080 import rospy
00081
00082
00083 class PlotView(MessageView):
00084
00085 """
00086 Popup plot viewer
00087 """
00088 name = 'Plot'
00089
00090 def __init__(self, timeline, parent, topic):
00091 super(PlotView, self).__init__(timeline, topic)
00092
00093 self.plot_widget = PlotWidget(timeline, parent, topic)
00094
00095 parent.layout().addWidget(self.plot_widget)
00096
00097 def message_viewed(self, bag, msg_details):
00098 """
00099 refreshes the plot
00100 """
00101 _, msg, t = msg_details[:3]
00102
00103 if t is None:
00104 self.message_cleared()
00105 else:
00106 self.plot_widget.message_tree.set_message(msg)
00107 self.plot_widget.set_cursor((t - self.plot_widget.start_stamp).to_sec())
00108
00109 def message_cleared(self):
00110 pass
00111
00112
00113 class PlotWidget(QWidget):
00114
00115 def __init__(self, timeline, parent, topic):
00116 super(PlotWidget, self).__init__(parent)
00117 self.setObjectName('PlotWidget')
00118
00119 self.timeline = timeline
00120 msg_type = self.timeline.get_datatype(topic)
00121 self.msgtopic = topic
00122 self.start_stamp = self.timeline._get_start_stamp()
00123 self.end_stamp = self.timeline._get_end_stamp()
00124
00125
00126
00127 self.limits = [0, (self.end_stamp - self.start_stamp).to_sec()]
00128
00129 rp = rospkg.RosPack()
00130 ui_file = os.path.join(rp.get_path('rqt_bag_plugins'), 'resource', 'plot.ui')
00131 loadUi(ui_file, self)
00132 self.message_tree = MessageTree(msg_type, self)
00133 self.data_tree_layout.addWidget(self.message_tree)
00134
00135
00136
00137 self.auto_res.stateChanged.connect(self.autoChanged)
00138
00139 self.resolution.editingFinished.connect(self.settingsChanged)
00140 self.resolution.setValidator(QDoubleValidator(0.0, 1000.0, 6, self.resolution))
00141
00142 self.timeline.selected_region_changed.connect(self.region_changed)
00143
00144 self.recompute_timestep()
00145
00146 self.plot = DataPlot(self)
00147 self.plot.set_autoscale(x=False)
00148 self.plot.set_autoscale(y=DataPlot.SCALE_VISIBLE)
00149 self.plot.autoscroll(False)
00150 self.plot.set_xlim(self.limits)
00151 self.data_plot_layout.addWidget(self.plot)
00152
00153 self._home_button = QPushButton()
00154 self._home_button.setToolTip("Reset View")
00155 self._home_button.setIcon(QIcon.fromTheme('go-home'))
00156 self._home_button.clicked.connect(self.home)
00157 self.plot_toolbar_layout.addWidget(self._home_button)
00158
00159 self._config_button = QPushButton("Configure Plot")
00160 self._config_button.clicked.connect(self.plot.doSettingsDialog)
00161 self.plot_toolbar_layout.addWidget(self._config_button)
00162
00163 self.set_cursor(0)
00164
00165 self.paths_on = set()
00166 self._lines = None
00167
00168
00169 bag = None
00170 start_time = self.start_stamp
00171 while bag is None:
00172 bag, entry = self.timeline.get_entry(start_time, topic)
00173 if bag is None:
00174 start_time = self.timeline.get_entry_after(start_time)[1].time
00175
00176 self.bag = bag
00177
00178 msg = bag._read_message(entry.position)
00179 self.message_tree.set_message(msg[1])
00180
00181
00182 self.resampling_active = False
00183 self.resample_thread = None
00184 self.resample_fields = set()
00185
00186 def set_cursor(self, position):
00187 self.plot.vline(position, color=DataPlot.RED)
00188 self.plot.redraw()
00189
00190 def add_plot(self, path):
00191 self.resample_data([path])
00192
00193 def update_plot(self):
00194 if len(self.paths_on) > 0:
00195 self.resample_data(self.paths_on)
00196
00197 def remove_plot(self, path):
00198 self.plot.remove_curve(path)
00199 self.paths_on.remove(path)
00200 self.plot.redraw()
00201
00202 def load_data(self):
00203 """get a generator for the specified time range on our bag"""
00204 return self.bag.read_messages(self.msgtopic,
00205 self.start_stamp + rospy.Duration.from_sec(self.limits[0]),
00206 self.start_stamp + rospy.Duration.from_sec(self.limits[1]))
00207
00208 def resample_data(self, fields):
00209 if self.resample_thread:
00210
00211 self.resampling_active = False
00212 self.resample_thread.join()
00213
00214 for f in fields:
00215 self.resample_fields.add(f)
00216
00217
00218 self.resampling_active = True
00219 self.resample_thread = threading.Thread(target=self._resample_thread)
00220
00221
00222 self.resample_thread.setDaemon(True)
00223 self.resample_thread.start()
00224
00225 def _resample_thread(self):
00226
00227
00228
00229
00230 x = {}
00231 y = {}
00232 for path in self.resample_fields:
00233 x[path] = []
00234 y[path] = []
00235
00236
00237 with self.timeline._bag_lock:
00238 try:
00239 msgdata = self.load_data()
00240 except ValueError:
00241
00242 self.resampling_active = False
00243 return
00244
00245 for entry in msgdata:
00246
00247 if not self.resampling_active:
00248 return
00249
00250 for path in self.resample_fields:
00251
00252
00253
00254
00255
00256 if x[path] == [] or (entry[2] - self.start_stamp).to_sec() - x[path][-1] >= self.timestep:
00257 y_value = entry[1]
00258 for field in path.split('.'):
00259 index = None
00260 if field.endswith(']'):
00261 field = field[:-1]
00262 field, _, index = field.rpartition('[')
00263 y_value = getattr(y_value, field)
00264 if index:
00265 index = int(index)
00266 y_value = y_value[index]
00267 y[path].append(y_value)
00268 x[path].append((entry[2] - self.start_stamp).to_sec())
00269
00270
00271
00272
00273
00274
00275
00276
00277
00278
00279
00280 for path in self.resample_fields:
00281 if len(x[path]) < 1:
00282 qWarning("Resampling resulted in 0 data points for %s" % path)
00283 else:
00284 if path in self.paths_on:
00285 self.plot.clear_values(path)
00286 self.plot.update_values(path, x[path], y[path])
00287 else:
00288 self.plot.add_curve(path, path, x[path], y[path])
00289 self.paths_on.add(path)
00290
00291 self.plot.redraw()
00292
00293 self.resample_fields.clear()
00294 self.resampling_active = False
00295
00296 def recompute_timestep(self):
00297
00298
00299 limits = self.limits
00300 if self.auto_res.isChecked():
00301 timestep = round((limits[1] - limits[0]) / 200.0, 5)
00302 else:
00303 timestep = float(self.resolution.text())
00304 self.resolution.setText(str(timestep))
00305 self.timestep = timestep
00306
00307 def region_changed(self, start, end):
00308
00309 limits = [(start - self.start_stamp).to_sec(),
00310 (end - self.start_stamp).to_sec()]
00311
00312
00313 if limits[0] < 0:
00314 limits = [0.0, limits[1]]
00315 if limits[1] > (self.end_stamp - self.start_stamp).to_sec():
00316 limits = [limits[0], (self.end_stamp - self.start_stamp).to_sec()]
00317
00318 self.limits = limits
00319
00320 self.recompute_timestep()
00321 self.plot.set_xlim(limits)
00322 self.plot.redraw()
00323 self.update_plot()
00324
00325 def settingsChanged(self):
00326
00327 self.recompute_timestep()
00328 self.update_plot()
00329
00330 def autoChanged(self, state):
00331 if state == 2:
00332
00333 self.resolution.setDisabled(True)
00334 self.recompute_timestep()
00335 self.update_plot()
00336 else:
00337
00338
00339 self.resolution.setDisabled(False)
00340
00341 def home(self):
00342
00343
00344
00345
00346
00347 self.plot.set_xlim(self.limits)
00348
00349 self.plot.redraw()
00350
00351
00352 class MessageTree(QTreeWidget):
00353
00354 def __init__(self, msg_type, parent):
00355 super(MessageTree, self).__init__(parent)
00356 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
00357 self.setHeaderHidden(True)
00358 self.itemChanged.connect(self.handleChanged)
00359 self._msg_type = msg_type
00360 self._msg = None
00361
00362 self._expanded_paths = None
00363 self._checked_states = set()
00364 self.plot_list = set()
00365
00366
00367
00368 @property
00369 def msg(self):
00370 return self._msg
00371
00372 def set_message(self, msg):
00373
00374 if self._msg:
00375 for item in self.get_all_items():
00376 path = self.get_item_path(item)
00377 if item.isExpanded():
00378 self._expanded_paths.add(path)
00379 elif path in self._expanded_paths:
00380 self._expanded_paths.remove(path)
00381 if item.checkState(0) == Qt.Checked:
00382 self._checked_states.add(path)
00383 elif path in self._checked_states:
00384 self._checked_states.remove(path)
00385 self.clear()
00386 if msg:
00387
00388 self._add_msg_object(None, '', '', msg, msg._type)
00389
00390 if self._expanded_paths is None:
00391 self._expanded_paths = set()
00392 else:
00393
00394
00395 for item in self.get_all_items():
00396 path = self.get_item_path(item)
00397 if path in self._expanded_paths:
00398 item.setExpanded(True)
00399 else:
00400 item.setExpanded(False)
00401 self._msg = msg
00402 self.update()
00403
00404 def get_item_path(self, item):
00405 return item.data(0, Qt.UserRole)[0].replace(' ', '')
00406
00407 def get_all_items(self):
00408 items = []
00409 try:
00410 root = self.invisibleRootItem()
00411 self.traverse(root, items.append)
00412 except Exception:
00413
00414 pass
00415 return items
00416
00417 def traverse(self, root, function):
00418 for i in range(root.childCount()):
00419 child = root.child(i)
00420 function(child)
00421 self.traverse(child, function)
00422
00423 def _add_msg_object(self, parent, path, name, obj, obj_type):
00424 label = name
00425
00426 if hasattr(obj, '__slots__'):
00427 subobjs = [(slot, getattr(obj, slot)) for slot in obj.__slots__]
00428 elif type(obj) in [list, tuple]:
00429 len_obj = len(obj)
00430 if len_obj == 0:
00431 subobjs = []
00432 else:
00433 w = int(math.ceil(math.log10(len_obj)))
00434 subobjs = [('[%*d]' % (w, i), subobj) for (i, subobj) in enumerate(obj)]
00435 else:
00436 subobjs = []
00437
00438 plotitem = False
00439 if type(obj) in [int, long, float]:
00440 plotitem = True
00441 if type(obj) == float:
00442 obj_repr = '%.6f' % obj
00443 else:
00444 obj_repr = str(obj)
00445
00446 if obj_repr[0] == '-':
00447 label += ': %s' % obj_repr
00448 else:
00449 label += ': %s' % obj_repr
00450
00451 elif type(obj) in [str, bool, int, long, float, complex, rospy.Time]:
00452
00453 obj_repr = codecs.utf_8_decode(str(obj), 'ignore')[0]
00454
00455
00456 if len(obj_repr) >= 50:
00457 obj_repr = obj_repr[:50] + '...'
00458
00459 label += ': ' + obj_repr
00460 item = QTreeWidgetItem([label])
00461 if name == '':
00462 pass
00463 elif path.find('.') == -1 and path.find('[') == -1:
00464 self.addTopLevelItem(item)
00465 else:
00466 parent.addChild(item)
00467 if plotitem == True:
00468 if path.replace(' ', '') in self._checked_states:
00469 item.setCheckState(0, Qt.Checked)
00470 else:
00471 item.setCheckState(0, Qt.Unchecked)
00472 item.setData(0, Qt.UserRole, (path, obj_type))
00473
00474 for subobj_name, subobj in subobjs:
00475 if subobj is None:
00476 continue
00477
00478 if path == '':
00479 subpath = subobj_name
00480 elif subobj_name.startswith('['):
00481 subpath = '%s%s' % (path, subobj_name)
00482 else:
00483 subpath = '%s.%s' % (path, subobj_name)
00484
00485 if hasattr(subobj, '_type'):
00486 subobj_type = subobj._type
00487 else:
00488 subobj_type = type(subobj).__name__
00489
00490 self._add_msg_object(item, subpath, subobj_name, subobj, subobj_type)
00491
00492 def handleChanged(self, item, column):
00493 if item.data(0, Qt.UserRole) == None:
00494 pass
00495 else:
00496 path = self.get_item_path(item)
00497 if item.checkState(column) == Qt.Checked:
00498 if path not in self.plot_list:
00499 self.plot_list.add(path)
00500 self.parent().parent().parent().add_plot(path)
00501 if item.checkState(column) == Qt.Unchecked:
00502 if path in self.plot_list:
00503 self.plot_list.remove(path)
00504 self.parent().parent().parent().remove_plot(path)