plot.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
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  # <-- Note the capitalization!
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 # Support both qt4 and qt5
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         # work around bug in dateutil
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         # work around bug in dateutil
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             # self.fig = fig = plt.figure()
00087             self.axes = self.figure.add_subplot(111, projection='3d')
00088             # self.axes = self.figure.gca(projection="3d")
00089             # self.axes.grid(True, color='gray')
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         # line = self._canvas.axes.plot(
00133         #     [], [], label=curve_name, linewidth=1, picker=5,
00134         #     color=color.name())[0]
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         # Set axis bounds
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             # pad the min/max
00201             # delta = max(ymax - ymin, 0.1)
00202             # ymin -= .05 * delta
00203             # ymax += .05 * delta
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         # create poly object
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             # c_topics is the list of topics to plot
00254             c_topics = []
00255             # compute combined topic list, t == '/foo/bar1,/baz/bar2'
00256             for sub_t in [x for x in t.split(',') if x]:
00257                 # check for shorthand '/foo/field1:field2:field3'
00258                 if ':' in sub_t:
00259                     base = sub_t[:sub_t.find(':')]
00260                     # the first prefix includes a field name,
00261                     # so save then strip it off
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                     # compute the rest of the field names
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             # #1053: resolve command-line topic names
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         # group.add_argument(
00301         #     '-e', '--empty', action='store_true', dest='start_empty',
00302         #     help='Start without restoring previous topics')
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         # init and start update timer for plot
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         # get topic name
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')  # NOQA
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_)')  # NOQA
00366                 return
00367         else:
00368             topic_name = str(event.mimeData().text())
00369 
00370         # check for numeric field type
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         # on empty topic name, update topics
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             # if pause button is not pressed,
00437             # enable timer based on subscribed topics
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)  # NOQA
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()


jsk_rqt_plugins
Author(s):
autogenerated on Wed May 1 2019 02:40:16