plot_widget.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #   * Redistributions of source code must retain the above copyright
00011 #     notice, this list of conditions and the following disclaimer.
00012 #   * Redistributions in binary form must reproduce the above
00013 #     copyright notice, this list of conditions and the following
00014 #     disclaimer in the documentation and/or other materials provided
00015 #     with the distribution.
00016 #   * Neither the name of the TU Darmstadt nor the names of its
00017 #     contributors may be used to endorse or promote products derived
00018 #     from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
00032 
00033 import os
00034 import rospkg
00035 import roslib
00036 
00037 from python_qt_binding import loadUi
00038 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Slot
00039 from python_qt_binding.QtGui import QAction, QIcon, QMenu, QWidget
00040 
00041 import rospy
00042 
00043 from rqt_py_common.topic_completer import TopicCompleter
00044 from rqt_py_common import topic_helpers
00045 
00046 from . rosplot import ROSData, RosPlotException
00047 
00048 def get_plot_fields(topic_name):
00049     topic_type, real_topic, _ = topic_helpers.get_topic_type(topic_name)
00050     if topic_type is None:
00051         message = "topic %s does not exist" % ( topic_name )
00052         return [], message
00053     field_name = topic_name[len(real_topic)+1:]
00054 
00055     slot_type, is_array, array_size = roslib.msgs.parse_type(topic_type)
00056     field_class = roslib.message.get_message_class(slot_type)
00057 
00058     fields = [f for f in field_name.split('/') if f]
00059 
00060     for field in fields:
00061         # parse the field name for an array index
00062         try:
00063             field, _, field_index = roslib.msgs.parse_type(field)
00064         except roslib.msgs.MsgSpecException:
00065             message = "invalid field %s in topic %s" % ( field, real_topic )
00066             return [], message
00067 
00068         if field not in getattr(field_class, '__slots__', []):
00069             message = "no field %s in topic %s" % ( field_name, real_topic )
00070             return [], message
00071         slot_type = field_class._slot_types[field_class.__slots__.index(field)]
00072         slot_type, slot_is_array, array_size = roslib.msgs.parse_type(slot_type)
00073         is_array = slot_is_array and field_index is None
00074 
00075         field_class = topic_helpers.get_type_class(slot_type)
00076 
00077     if field_class in (int, float):
00078         if is_array:
00079             if array_size is not None:
00080                 message = "topic %s is fixed-size numeric array" % ( topic_name )
00081                 return [ "%s[%d]" % (topic_name, i) for i in range(array_size) ], message
00082             else:
00083                 message = "topic %s is variable-size numeric array" % ( topic_name )
00084                 return [], message
00085         else:
00086             message = "topic %s is numeric" % ( topic_name )
00087             return [ topic_name ], message
00088     else:
00089         if not roslib.msgs.is_valid_constant_type(slot_type):
00090             numeric_fields = []
00091             for i, slot in enumerate(field_class.__slots__):
00092                 slot_type = field_class._slot_types[i]
00093                 slot_type, is_array, array_size = roslib.msgs.parse_type(slot_type)
00094                 slot_class = topic_helpers.get_type_class(slot_type)
00095                 if slot_class in (int, float) and not is_array:
00096                     numeric_fields.append(slot)
00097             message = ""
00098             if len(numeric_fields) > 0:
00099                 message = "%d plottable fields in %s" % ( len(numeric_fields), topic_name )
00100             else:
00101                 message = "No plottable fields in %s" % ( topic_name )
00102             return [ "%s/%s" % (topic_name, f) for f in numeric_fields ], message
00103         else:
00104             message = "Topic %s is not numeric" % ( topic_name )
00105             return [], message
00106 
00107 def is_plottable(topic_name):
00108     fields, message = get_plot_fields(topic_name)
00109     return len(fields) > 0, message
00110 
00111 class PlotWidget(QWidget):
00112     _redraw_interval = 40
00113 
00114     def __init__(self, initial_topics=None, start_paused=False):
00115         super(PlotWidget, self).__init__()
00116         self.setObjectName('PlotWidget')
00117 
00118         self._initial_topics = initial_topics
00119 
00120         rp = rospkg.RosPack()
00121         ui_file = os.path.join(rp.get_path('rqt_plot'), 'resource', 'plot.ui')
00122         loadUi(ui_file, self)
00123         self.subscribe_topic_button.setIcon(QIcon.fromTheme('list-add'))
00124         self.remove_topic_button.setIcon(QIcon.fromTheme('list-remove'))
00125         self.pause_button.setIcon(QIcon.fromTheme('media-playback-pause'))
00126         self.clear_button.setIcon(QIcon.fromTheme('edit-clear'))
00127         self.data_plot = None
00128 
00129         self.subscribe_topic_button.setEnabled(False)
00130         if start_paused:
00131             self.pause_button.setChecked(True)
00132 
00133         self._topic_completer = TopicCompleter(self.topic_edit)
00134         self._topic_completer.update_topics()
00135         self.topic_edit.setCompleter(self._topic_completer)
00136 
00137         self._start_time = rospy.get_time()
00138         self._rosdata = {}
00139         self._remove_topic_menu = QMenu()
00140 
00141         # init and start update timer for plot
00142         self._update_plot_timer = QTimer(self)
00143         self._update_plot_timer.timeout.connect(self.update_plot)
00144 
00145     def switch_data_plot_widget(self, data_plot):
00146         self.enable_timer(enabled=False)
00147 
00148         self.data_plot_layout.removeWidget(self.data_plot)
00149         if self.data_plot is not None:
00150             self.data_plot.close()
00151 
00152         self.data_plot = data_plot
00153         self.data_plot_layout.addWidget(self.data_plot)
00154         self.data_plot.autoscroll(self.autoscroll_checkbox.isChecked())
00155 
00156         # setup drag 'n drop
00157         self.data_plot.dropEvent = self.dropEvent
00158         self.data_plot.dragEnterEvent = self.dragEnterEvent
00159 
00160         if self._initial_topics:
00161             for topic_name in self._initial_topics:
00162                 self.add_topic(topic_name)
00163             self._initial_topics = None
00164         else:
00165             for topic_name, rosdata in self._rosdata.items():
00166                 data_x, data_y = rosdata.next()
00167                 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00168 
00169         self._subscribed_topics_changed()
00170 
00171     @Slot('QDragEnterEvent*')
00172     def dragEnterEvent(self, event):
00173         # get topic name
00174         if not event.mimeData().hasText():
00175             if not hasattr(event.source(), 'selectedItems') or len(event.source().selectedItems()) == 0:
00176                 qWarning('Plot.dragEnterEvent(): not hasattr(event.source(), selectedItems) or len(event.source().selectedItems()) == 0')
00177                 return
00178             item = event.source().selectedItems()[0]
00179             topic_name = item.data(0, Qt.UserRole)
00180             if topic_name == None:
00181                 qWarning('Plot.dragEnterEvent(): not hasattr(item, ros_topic_name_)')
00182                 return
00183         else:
00184             topic_name = str(event.mimeData().text())
00185 
00186         # check for plottable field type
00187         plottable, message = is_plottable(topic_name)
00188         if plottable:
00189             event.acceptProposedAction()
00190         else:
00191             qWarning('Plot.dragEnterEvent(): rejecting: "%s"' % (message))
00192 
00193     @Slot('QDropEvent*')
00194     def dropEvent(self, event):
00195         if event.mimeData().hasText():
00196             topic_name = str(event.mimeData().text())
00197         else:
00198             droped_item = event.source().selectedItems()[0]
00199             topic_name = str(droped_item.data(0, Qt.UserRole))
00200         self.add_topic(topic_name)
00201 
00202     @Slot(str)
00203     def on_topic_edit_textChanged(self, topic_name):
00204         # on empty topic name, update topics
00205         if topic_name in ('', '/'):
00206             self._topic_completer.update_topics()
00207 
00208         plottable, message = is_plottable(topic_name)
00209         self.subscribe_topic_button.setEnabled(plottable)
00210         self.subscribe_topic_button.setToolTip(message)
00211 
00212     @Slot()
00213     def on_topic_edit_returnPressed(self):
00214         if self.subscribe_topic_button.isEnabled():
00215             self.add_topic(str(self.topic_edit.text()))
00216 
00217     @Slot()
00218     def on_subscribe_topic_button_clicked(self):
00219         self.add_topic(str(self.topic_edit.text()))
00220 
00221     @Slot(bool)
00222     def on_pause_button_clicked(self, checked):
00223         self.enable_timer(not checked)
00224 
00225     @Slot(bool)
00226     def on_autoscroll_checkbox_clicked(self, checked):
00227         self.data_plot.autoscroll(checked)
00228         if checked:
00229             self.data_plot.redraw()
00230 
00231     @Slot()
00232     def on_clear_button_clicked(self):
00233         self.clear_plot()
00234 
00235     def update_plot(self):
00236         if self.data_plot is not None:
00237             needs_redraw = False
00238             for topic_name, rosdata in self._rosdata.items():
00239                 try:
00240                     data_x, data_y = rosdata.next()
00241                     if data_x or data_y:
00242                         self.data_plot.update_values(topic_name, data_x, data_y)
00243                         needs_redraw = True
00244                 except RosPlotException as e:
00245                     qWarning('PlotWidget.update_plot(): error in rosplot: %s' % e)
00246             if needs_redraw:
00247                 self.data_plot.redraw()
00248 
00249     def _subscribed_topics_changed(self):
00250         self._update_remove_topic_menu()
00251         if not self.pause_button.isChecked():
00252             # if pause button is not pressed, enable timer based on subscribed topics
00253             self.enable_timer(self._rosdata)
00254         self.data_plot.redraw()
00255 
00256     def _update_remove_topic_menu(self):
00257         def make_remove_topic_function(x):
00258             return lambda: self.remove_topic(x)
00259 
00260         self._remove_topic_menu.clear()
00261         for topic_name in sorted(self._rosdata.keys()):
00262             action = QAction(topic_name, self._remove_topic_menu)
00263             action.triggered.connect(make_remove_topic_function(topic_name))
00264             self._remove_topic_menu.addAction(action)
00265 
00266         if len(self._rosdata) > 1:
00267             all_action = QAction('All', self._remove_topic_menu)
00268             all_action.triggered.connect(self.clean_up_subscribers)
00269             self._remove_topic_menu.addAction(all_action)
00270 
00271         self.remove_topic_button.setMenu(self._remove_topic_menu)
00272 
00273     def add_topic(self, topic_name):
00274         topics_changed = False
00275         for topic_name in get_plot_fields(topic_name)[0]:
00276             if topic_name in self._rosdata:
00277                 qWarning('PlotWidget.add_topic(): topic already subscribed: %s' % topic_name)
00278                 continue
00279             self._rosdata[topic_name] = ROSData(topic_name, self._start_time)
00280             if self._rosdata[topic_name].error is not None:
00281                 qWarning(str(self._rosdata[topic_name].error))
00282                 del self._rosdata[topic_name]
00283             else:
00284                 data_x, data_y = self._rosdata[topic_name].next()
00285                 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00286                 topics_changed = True
00287 
00288         if topics_changed:
00289             self._subscribed_topics_changed()
00290 
00291     def remove_topic(self, topic_name):
00292         self._rosdata[topic_name].close()
00293         del self._rosdata[topic_name]
00294         self.data_plot.remove_curve(topic_name)
00295 
00296         self._subscribed_topics_changed()
00297 
00298     def clear_plot(self):
00299         for topic_name, _ in self._rosdata.items():
00300             self.data_plot.clear_values(topic_name)
00301         self.data_plot.redraw()
00302 
00303     def clean_up_subscribers(self):
00304         for topic_name, rosdata in self._rosdata.items():
00305             rosdata.close()
00306             self.data_plot.remove_curve(topic_name)
00307         self._rosdata = {}
00308 
00309         self._subscribed_topics_changed()
00310 
00311     def enable_timer(self, enabled=True):
00312         if enabled:
00313             self._update_plot_timer.start(self._redraw_interval)
00314         else:
00315             self._update_plot_timer.stop()


rqt_plot
Author(s): Dorian Scholz
autogenerated on Wed Sep 16 2015 06:58:13