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