00001
00002
00003 from rqt_gui_py.plugin import Plugin
00004 from python_qt_binding import loadUi
00005 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Slot
00006 from python_qt_binding.QtGui import QAction, QIcon, QMenu, QWidget
00007 from python_qt_binding.QtGui import QWidget, QVBoxLayout, QSizePolicy, QColor
00008 from rqt_py_common.topic_completer import TopicCompleter
00009 from matplotlib.colors import colorConverter
00010 from rqt_py_common.topic_helpers import is_slot_numeric
00011 from rqt_plot.rosplot import ROSData, RosPlotException
00012 from matplotlib.collections import PolyCollection, PathCollection, LineCollection
00013 import matplotlib
00014
00015 import rospkg
00016 import rospy
00017
00018 import os, sys
00019 import argparse
00020
00021 try:
00022 from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
00023 except ImportError:
00024
00025 import sys
00026 import thread
00027 sys.modules['_thread'] = thread
00028 from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
00029 from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
00030 from matplotlib.figure import Figure
00031
00032 import numpy
00033 import matplotlib.pyplot as plt
00034 from mpl_toolkits.mplot3d import axes3d, Axes3D
00035
00036 class MatDataPlot3D(QWidget):
00037 class Canvas(FigureCanvas):
00038 """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
00039 def __init__(self, parent=None):
00040 super(MatDataPlot3D.Canvas, self).__init__(Figure())
00041
00042 self.axes = self.figure.add_subplot(111, projection='3d')
00043
00044
00045 self.axes.set_xlabel('t')
00046 self.axes.set_xlim3d(0, 10)
00047 self.axes.set_ylabel('Y')
00048 self.axes.set_ylim3d(-1, 1)
00049 self.axes.set_zlabel('Z')
00050 self.axes.set_zlim3d(0, 1)
00051
00052 self.figure.tight_layout()
00053 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
00054 self.updateGeometry()
00055
00056 def resizeEvent(self, event):
00057 super(MatDataPlot3D.Canvas, self).resizeEvent(event)
00058 self.figure.tight_layout()
00059
00060 _colors = [QColor(c) for c in [Qt.red, Qt.blue, Qt.magenta, Qt.cyan, Qt.green, Qt.darkYellow, Qt.black, Qt.darkRed, Qt.gray, Qt.darkCyan]]
00061
00062 def __init__(self, parent=None, buffer_length=100, use_poly=True,
00063 no_legend=False):
00064 super(MatDataPlot3D, self).__init__(parent)
00065 self._canvas = MatDataPlot3D.Canvas()
00066 self._use_poly = use_poly
00067 self._buffer_length = buffer_length
00068 self._toolbar = NavigationToolbar(self._canvas, self._canvas)
00069 vbox = QVBoxLayout()
00070 vbox.addWidget(self._toolbar)
00071 vbox.addWidget(self._canvas)
00072 self.setLayout(vbox)
00073 self._curves_verts = {}
00074 self._color_index = 0
00075 self._curves = {}
00076 self._no_legend = no_legend
00077 self._autoscroll = False
00078
00079 def autoscroll(self, enabled=True):
00080 self._autoscroll = enabled
00081
00082 def add_curve(self, curve_id, curve_name, x, y):
00083 color = QColor(self._colors[self._color_index % len(self._colors)])
00084 self._color_index += 1
00085
00086 line = None
00087 self._curves[curve_id] = [[], [], line, [None, None],
00088 (color.red() / 255.0,
00089 color.green() / 255.0,
00090 color.blue() / 255.0,
00091 0.6)]
00092 self.update_values(curve_id, x, y)
00093 self._update_legend()
00094
00095 def remove_curve(self, curve_id):
00096 curve_id = str(curve_id)
00097 if curve_id in self._curves:
00098 del self._curves[curve_id]
00099 del self._curves_verts[curve_id]
00100 self._update_legend()
00101
00102 def _update_legend(self):
00103 if self._no_legend:
00104 return
00105 labels = self._curves.keys()
00106 handles = [plt.Rectangle((0, 0), 1, 1, fc=self._curves[labels[i]][4] ) for i in range(len(labels))]
00107 self._canvas.axes.legend(handles, labels, loc='upper left')
00108
00109 @Slot(str, list, list)
00110 def update_values(self, curve_id, x, y):
00111 data_x, data_y, line, range_y,c = self._curves[curve_id]
00112 data_x.extend(x)
00113 data_y.extend(y)
00114 if len(data_x) > self._buffer_length:
00115 data_x = data_x[-self._buffer_length:]
00116 data_y = data_y[-self._buffer_length:]
00117 self._curves[curve_id][0] = data_x
00118 self._curves[curve_id][1] = data_y
00119 self._curves_verts[curve_id] = (data_x, data_y)
00120 if y:
00121 ymin = min(y)
00122 if range_y[0]:
00123 ymin = min(ymin, range_y[0])
00124 range_y[0] = ymin
00125 ymax = max(y)
00126 if range_y[1]:
00127 ymax = max(ymax, range_y[1])
00128 range_y[1] = ymax
00129 def redraw(self):
00130 self._canvas.axes.grid(True, color='gray')
00131
00132 ymin = ymax = None
00133 xmax = 0
00134 xmin = sys.maxint
00135 for curve in self._curves.values():
00136 data_x, _, _, range_y, c = curve
00137 if len(data_x) == 0:
00138 continue
00139 xmax = max(xmax, data_x[-1])
00140 xmin = min(xmin, data_x[0])
00141 if ymin is None:
00142 ymin = range_y[0]
00143 ymax = range_y[1]
00144 else:
00145 ymin = min(range_y[0], ymin)
00146 ymax = max(range_y[1], ymax)
00147
00148
00149
00150
00151
00152
00153 if self._autoscroll and ymin is not None:
00154 self._canvas.axes.set_xbound(lower=xmin, upper=xmax)
00155 self._canvas.axes.set_zbound(lower=ymin, upper=ymax)
00156 self._canvas.axes.set_ybound(lower=0,
00157 upper=len(self._curves.keys()))
00158
00159 verts = []
00160 colors = []
00161 for curve_id in self._curves_verts.keys():
00162 (data_x, data_y) = self._curves_verts[curve_id]
00163 colors.append(self._curves[curve_id][4])
00164 if self._use_poly:
00165 verts.append([(xmin, ymin)] + list(zip(data_x, data_y))
00166 + [(xmax, ymin)])
00167 else:
00168 verts.append(zip(data_x, data_y))
00169 line_num = len(self._curves.keys())
00170 if self._use_poly:
00171 poly = PolyCollection(verts, facecolors=colors, closed=False)
00172 else:
00173 poly = LineCollection(verts, colors=colors)
00174 poly.set_alpha(0.7)
00175 self._canvas.axes.cla()
00176 self._canvas.axes.add_collection3d(poly,
00177 zs=range(line_num), zdir='y')
00178 self._update_legend()
00179 self._canvas.draw()
00180
00181
00182 class Plot3D(Plugin):
00183 def __init__(self, context):
00184 super(Plot3D, self).__init__(context)
00185 self.setObjectName('Plot3D')
00186 self._args = self._parse_args(context.argv())
00187 self._widget = Plot3DWidget(initial_topics=self._args.topics, start_paused=self._args.start_paused,
00188 buffer_length=self._args.buffer,
00189 use_poly=not self._args.show_line,
00190 no_legend=self._args.no_legend)
00191 context.add_widget(self._widget)
00192 def _parse_args(self, argv):
00193 parser = argparse.ArgumentParser(prog='rqt_3d_plot', add_help=False)
00194 Plot3D.add_arguments(parser)
00195 args = parser.parse_args(argv)
00196 topic_list = []
00197 for t in args.topics:
00198
00199 c_topics = []
00200
00201 for sub_t in [x for x in t.split(',') if x]:
00202
00203 if ':' in sub_t:
00204 base = sub_t[:sub_t.find(':')]
00205
00206 c_topics.append(base)
00207 if not '/' in base:
00208 parser.error("%s must contain a topic and field name" % sub_t)
00209 base = base[:base.rfind('/')]
00210
00211
00212 fields = sub_t.split(':')[1:]
00213 c_topics.extend(["%s/%s" % (base, f) for f in fields if f])
00214 else:
00215 c_topics.append(sub_t)
00216
00217 import rosgraph
00218 c_topics = [rosgraph.names.script_resolve_name('rqt_plot', n) for n in c_topics]
00219 if type(c_topics) == list:
00220 topic_list.extend(c_topics)
00221 else:
00222 topic_list.append(c_topics)
00223 args.topics = topic_list
00224
00225 return args
00226 @staticmethod
00227 def add_arguments(parser):
00228 group = parser.add_argument_group('Options for rqt_plot plugin')
00229 group.add_argument('-P', '--pause', action='store_true', dest='start_paused',
00230 help='Start in paused state')
00231 group.add_argument('-L', '--line', action='store_true', dest='show_line',
00232 help='Show lines rather than polygon representation')
00233 group.add_argument('--no-legend', action='store_true', dest='no_legend',
00234 help='do not show legend')
00235 group.add_argument('-B', '--buffer', dest='buffer', action="store",
00236 help='the length of the buffer', default=100, type=int)
00237
00238
00239 group.add_argument('topics', nargs='*', default=[], help='Topics to plot')
00240
00241 class Plot3DWidget(QWidget):
00242 _redraw_interval = 40
00243
00244 def __init__(self, initial_topics=None, start_paused=False,
00245 buffer_length=100, use_poly=True, no_legend=False):
00246 super(Plot3DWidget, self).__init__()
00247 self.setObjectName('Plot3DWidget')
00248 self._buffer_length = buffer_length
00249 self._initial_topics = initial_topics
00250
00251 rp = rospkg.RosPack()
00252 ui_file = os.path.join(rp.get_path('jsk_rqt_plugins'),
00253 'resource', 'plot3d.ui')
00254 loadUi(ui_file, self)
00255 self.subscribe_topic_button.setIcon(QIcon.fromTheme('add'))
00256 self.remove_topic_button.setIcon(QIcon.fromTheme('remove'))
00257 self.pause_button.setIcon(QIcon.fromTheme('media-playback-pause'))
00258 self.clear_button.setIcon(QIcon.fromTheme('edit-clear'))
00259 self.data_plot = MatDataPlot3D(self, self._buffer_length,
00260 use_poly, no_legend)
00261 self.data_plot_layout.addWidget(self.data_plot)
00262 self.data_plot.autoscroll(self.autoscroll_checkbox.isChecked())
00263 self.data_plot.dropEvent = self.dropEvent
00264 self.data_plot.dragEnterEvent = self.dragEnterEvent
00265
00266 self.subscribe_topic_button.setEnabled(False)
00267 if start_paused:
00268 self.pause_button.setChecked(True)
00269
00270 self._topic_completer = TopicCompleter(self.topic_edit)
00271 self._topic_completer.update_topics()
00272 self.topic_edit.setCompleter(self._topic_completer)
00273
00274 self._start_time = rospy.get_time()
00275 self._rosdata = {}
00276 self._remove_topic_menu = QMenu()
00277
00278
00279 self._update_plot_timer = QTimer(self)
00280 self._update_plot_timer.timeout.connect(self.update_plot)
00281 if self._initial_topics:
00282 for topic_name in self._initial_topics:
00283 self.add_topic(topic_name)
00284 self._initial_topics = None
00285
00286 @Slot('QDragEnterEvent*')
00287 def dragEnterEvent(self, event):
00288
00289 if not event.mimeData().hasText():
00290 if not hasattr(event.source(), 'selectedItems') or len(event.source().selectedItems()) == 0:
00291 qWarning('Plot.dragEnterEvent(): not hasattr(event.source(), selectedItems) or len(event.source().selectedItems()) == 0')
00292 return
00293 item = event.source().selectedItems()[0]
00294 topic_name = item.data(0, Qt.UserRole)
00295 if topic_name == None:
00296 qWarning('Plot.dragEnterEvent(): not hasattr(item, ros_topic_name_)')
00297 return
00298 else:
00299 topic_name = str(event.mimeData().text())
00300
00301
00302 is_numeric, is_array, message = is_slot_numeric(topic_name)
00303 if is_numeric and not is_array:
00304 event.acceptProposedAction()
00305 else:
00306 qWarning('Plot.dragEnterEvent(): rejecting: "%s"' % (message))
00307
00308 @Slot('QDropEvent*')
00309 def dropEvent(self, event):
00310 if event.mimeData().hasText():
00311 topic_name = str(event.mimeData().text())
00312 else:
00313 droped_item = event.source().selectedItems()[0]
00314 topic_name = str(droped_item.data(0, Qt.UserRole))
00315 self.add_topic(topic_name)
00316
00317 @Slot(str)
00318 def on_topic_edit_textChanged(self, topic_name):
00319
00320 if topic_name in ('', '/'):
00321 self._topic_completer.update_topics()
00322
00323 is_numeric, is_array, message = is_slot_numeric(topic_name)
00324 self.subscribe_topic_button.setEnabled(is_numeric and not is_array)
00325 self.subscribe_topic_button.setToolTip(message)
00326
00327 @Slot()
00328 def on_topic_edit_returnPressed(self):
00329 if self.subscribe_topic_button.isEnabled():
00330 self.add_topic(str(self.topic_edit.text()))
00331
00332 @Slot()
00333 def on_subscribe_topic_button_clicked(self):
00334 self.add_topic(str(self.topic_edit.text()))
00335
00336 @Slot(bool)
00337 def on_pause_button_clicked(self, checked):
00338 self.enable_timer(not checked)
00339
00340 @Slot(bool)
00341 def on_autoscroll_checkbox_clicked(self, checked):
00342 self.data_plot.autoscroll(checked)
00343
00344 @Slot()
00345 def on_clear_button_clicked(self):
00346 self.clean_up_subscribers()
00347
00348 def update_plot(self):
00349 if self.data_plot is not None:
00350 needs_redraw = False
00351 for topic_name, rosdata in self._rosdata.items():
00352 try:
00353 data_x, data_y = rosdata.next()
00354 if data_x or data_y:
00355 self.data_plot.update_values(topic_name, data_x, data_y)
00356 needs_redraw = True
00357 except RosPlotException as e:
00358 qWarning('PlotWidget.update_plot(): error in rosplot: %s' % e)
00359 if needs_redraw:
00360 self.data_plot.redraw()
00361
00362 def _subscribed_topics_changed(self):
00363 self._update_remove_topic_menu()
00364 if not self.pause_button.isChecked():
00365
00366 self.enable_timer(self._rosdata)
00367
00368 def _update_remove_topic_menu(self):
00369 def make_remove_topic_function(x):
00370 return lambda: self.remove_topic(x)
00371
00372 self._remove_topic_menu.clear()
00373 for topic_name in sorted(self._rosdata.keys()):
00374 action = QAction(topic_name, self._remove_topic_menu)
00375 action.triggered.connect(make_remove_topic_function(topic_name))
00376 self._remove_topic_menu.addAction(action)
00377
00378 self.remove_topic_button.setMenu(self._remove_topic_menu)
00379
00380 def add_topic(self, topic_name):
00381 if topic_name in self._rosdata:
00382 qWarning('PlotWidget.add_topic(): topic already subscribed: %s' % topic_name)
00383 return
00384
00385 self._rosdata[topic_name] = ROSData(topic_name, self._start_time)
00386 if self._rosdata[topic_name].error is not None:
00387 qWarning(str(self._rosdata[topic_name].error))
00388 del self._rosdata[topic_name]
00389 else:
00390 data_x, data_y = self._rosdata[topic_name].next()
00391 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00392
00393 self._subscribed_topics_changed()
00394
00395 def remove_topic(self, topic_name):
00396 self._rosdata[topic_name].close()
00397 del self._rosdata[topic_name]
00398 self.data_plot.remove_curve(topic_name)
00399
00400 self._subscribed_topics_changed()
00401
00402 def clean_up_subscribers(self):
00403 for topic_name, rosdata in self._rosdata.items():
00404 rosdata.close()
00405 self.data_plot.remove_curve(topic_name)
00406 self._rosdata = {}
00407
00408 self._subscribed_topics_changed()
00409
00410 def enable_timer(self, enabled=True):
00411 if enabled:
00412 self._update_plot_timer.start(self._redraw_interval)
00413 else:
00414 self._update_plot_timer.stop()