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