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 QIcon
00040 from python_qt_binding.QtWidgets import QAction, QMenu, QWidget
00041 
00042 import rospy
00043 
00044 from rqt_py_common.topic_completer import TopicCompleter
00045 from rqt_py_common import topic_helpers
00046 
00047 from . rosplot import ROSData, RosPlotException
00048 
00049 def get_plot_fields(topic_name):
00050     topic_type, real_topic, _ = topic_helpers.get_topic_type(topic_name)
00051     if topic_type is None:
00052         message = "topic %s does not exist" % ( topic_name )
00053         return [], message
00054     field_name = topic_name[len(real_topic)+1:]
00055 
00056     slot_type, is_array, array_size = roslib.msgs.parse_type(topic_type)
00057     field_class = roslib.message.get_message_class(slot_type)
00058 
00059     fields = [f for f in field_name.split('/') if f]
00060 
00061     for field in fields:
00062         # parse the field name for an array index
00063         try:
00064             field, _, field_index = roslib.msgs.parse_type(field)
00065         except roslib.msgs.MsgSpecException:
00066             message = "invalid field %s in topic %s" % ( field, real_topic )
00067             return [], message
00068 
00069         if field not in getattr(field_class, '__slots__', []):
00070             message = "no field %s in topic %s" % ( field_name, real_topic )
00071             return [], message
00072         slot_type = field_class._slot_types[field_class.__slots__.index(field)]
00073         slot_type, slot_is_array, array_size = roslib.msgs.parse_type(slot_type)
00074         is_array = slot_is_array and field_index is None
00075 
00076         field_class = topic_helpers.get_type_class(slot_type)
00077 
00078     if field_class in (int, float, bool):
00079         topic_kind = 'boolean' if field_class == bool else 'numeric'
00080         if is_array:
00081             if array_size is not None:
00082                 message = "topic %s is fixed-size %s array" % ( topic_name, topic_kind )
00083                 return [ "%s[%d]" % (topic_name, i) for i in range(array_size) ], message
00084             else:
00085                 message = "topic %s is variable-size %s array" % ( topic_name, topic_kind )
00086                 return [], message
00087         else:
00088             message = "topic %s is %s" % ( topic_name, topic_kind )
00089             return [ topic_name ], message
00090     else:
00091         if not roslib.msgs.is_valid_constant_type(slot_type):
00092             numeric_fields = []
00093             for i, slot in enumerate(field_class.__slots__):
00094                 slot_type = field_class._slot_types[i]
00095                 slot_type, is_array, array_size = roslib.msgs.parse_type(slot_type)
00096                 slot_class = topic_helpers.get_type_class(slot_type)
00097                 if slot_class in (int, float) and not is_array:
00098                     numeric_fields.append(slot)
00099             message = ""
00100             if len(numeric_fields) > 0:
00101                 message = "%d plottable fields in %s" % ( len(numeric_fields), topic_name )
00102             else:
00103                 message = "No plottable fields in %s" % ( topic_name )
00104             return [ "%s/%s" % (topic_name, f) for f in numeric_fields ], message
00105         else:
00106             message = "Topic %s is not numeric" % ( topic_name )
00107             return [], message
00108 
00109 def is_plottable(topic_name):
00110     fields, message = get_plot_fields(topic_name)
00111     return len(fields) > 0, message
00112 
00113 class PlotWidget(QWidget):
00114     _redraw_interval = 40
00115 
00116     def __init__(self, initial_topics=None, start_paused=False):
00117         super(PlotWidget, self).__init__()
00118         self.setObjectName('PlotWidget')
00119 
00120         self._initial_topics = initial_topics
00121 
00122         rp = rospkg.RosPack()
00123         ui_file = os.path.join(rp.get_path('rqt_plot'), 'resource', 'plot.ui')
00124         loadUi(ui_file, self)
00125         self.subscribe_topic_button.setIcon(QIcon.fromTheme('list-add'))
00126         self.remove_topic_button.setIcon(QIcon.fromTheme('list-remove'))
00127         self.pause_button.setIcon(QIcon.fromTheme('media-playback-pause'))
00128         self.clear_button.setIcon(QIcon.fromTheme('edit-clear'))
00129         self.data_plot = None
00130 
00131         self.subscribe_topic_button.setEnabled(False)
00132         if start_paused:
00133             self.pause_button.setChecked(True)
00134 
00135         self._topic_completer = TopicCompleter(self.topic_edit)
00136         self._topic_completer.update_topics()
00137         self.topic_edit.setCompleter(self._topic_completer)
00138 
00139         self._start_time = rospy.get_time()
00140         self._rosdata = {}
00141         self._remove_topic_menu = QMenu()
00142 
00143         # init and start update timer for plot
00144         self._update_plot_timer = QTimer(self)
00145         self._update_plot_timer.timeout.connect(self.update_plot)
00146 
00147     def switch_data_plot_widget(self, data_plot):
00148         self.enable_timer(enabled=False)
00149 
00150         self.data_plot_layout.removeWidget(self.data_plot)
00151         if self.data_plot is not None:
00152             self.data_plot.close()
00153 
00154         self.data_plot = data_plot
00155         self.data_plot_layout.addWidget(self.data_plot)
00156         self.data_plot.autoscroll(self.autoscroll_checkbox.isChecked())
00157 
00158         # setup drag 'n drop
00159         self.data_plot.dropEvent = self.dropEvent
00160         self.data_plot.dragEnterEvent = self.dragEnterEvent
00161 
00162         if self._initial_topics:
00163             for topic_name in self._initial_topics:
00164                 self.add_topic(topic_name)
00165             self._initial_topics = None
00166         else:
00167             for topic_name, rosdata in self._rosdata.items():
00168                 data_x, data_y = rosdata.next()
00169                 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00170 
00171         self._subscribed_topics_changed()
00172 
00173     @Slot('QDragEnterEvent*')
00174     def dragEnterEvent(self, event):
00175         # get topic name
00176         if not event.mimeData().hasText():
00177             if not hasattr(event.source(), 'selectedItems') or len(event.source().selectedItems()) == 0:
00178                 qWarning('Plot.dragEnterEvent(): not hasattr(event.source(), selectedItems) or len(event.source().selectedItems()) == 0')
00179                 return
00180             item = event.source().selectedItems()[0]
00181             topic_name = item.data(0, Qt.UserRole)
00182             if topic_name == None:
00183                 qWarning('Plot.dragEnterEvent(): not hasattr(item, ros_topic_name_)')
00184                 return
00185         else:
00186             topic_name = str(event.mimeData().text())
00187 
00188         # check for plottable field type
00189         plottable, message = is_plottable(topic_name)
00190         if plottable:
00191             event.acceptProposedAction()
00192         else:
00193             qWarning('Plot.dragEnterEvent(): rejecting: "%s"' % (message))
00194 
00195     @Slot('QDropEvent*')
00196     def dropEvent(self, event):
00197         if event.mimeData().hasText():
00198             topic_name = str(event.mimeData().text())
00199         else:
00200             droped_item = event.source().selectedItems()[0]
00201             topic_name = str(droped_item.data(0, Qt.UserRole))
00202         self.add_topic(topic_name)
00203 
00204     @Slot(str)
00205     def on_topic_edit_textChanged(self, topic_name):
00206         # on empty topic name, update topics
00207         if topic_name in ('', '/'):
00208             self._topic_completer.update_topics()
00209 
00210         plottable, message = is_plottable(topic_name)
00211         self.subscribe_topic_button.setEnabled(plottable)
00212         self.subscribe_topic_button.setToolTip(message)
00213 
00214     @Slot()
00215     def on_topic_edit_returnPressed(self):
00216         if self.subscribe_topic_button.isEnabled():
00217             self.add_topic(str(self.topic_edit.text()))
00218 
00219     @Slot()
00220     def on_subscribe_topic_button_clicked(self):
00221         self.add_topic(str(self.topic_edit.text()))
00222 
00223     @Slot(bool)
00224     def on_pause_button_clicked(self, checked):
00225         self.enable_timer(not checked)
00226 
00227     @Slot(bool)
00228     def on_autoscroll_checkbox_clicked(self, checked):
00229         self.data_plot.autoscroll(checked)
00230         if checked:
00231             self.data_plot.redraw()
00232 
00233     @Slot()
00234     def on_clear_button_clicked(self):
00235         self.clear_plot()
00236 
00237     def update_plot(self):
00238         if self.data_plot is not None:
00239             needs_redraw = False
00240             for topic_name, rosdata in self._rosdata.items():
00241                 try:
00242                     data_x, data_y = rosdata.next()
00243                     if data_x or data_y:
00244                         self.data_plot.update_values(topic_name, data_x, data_y)
00245                         needs_redraw = True
00246                 except RosPlotException as e:
00247                     qWarning('PlotWidget.update_plot(): error in rosplot: %s' % e)
00248             if needs_redraw:
00249                 self.data_plot.redraw()
00250 
00251     def _subscribed_topics_changed(self):
00252         self._update_remove_topic_menu()
00253         if not self.pause_button.isChecked():
00254             # if pause button is not pressed, enable timer based on subscribed topics
00255             self.enable_timer(self._rosdata)
00256         self.data_plot.redraw()
00257 
00258     def _update_remove_topic_menu(self):
00259         def make_remove_topic_function(x):
00260             return lambda: self.remove_topic(x)
00261 
00262         self._remove_topic_menu.clear()
00263         for topic_name in sorted(self._rosdata.keys()):
00264             action = QAction(topic_name, self._remove_topic_menu)
00265             action.triggered.connect(make_remove_topic_function(topic_name))
00266             self._remove_topic_menu.addAction(action)
00267 
00268         if len(self._rosdata) > 1:
00269             all_action = QAction('All', self._remove_topic_menu)
00270             all_action.triggered.connect(self.clean_up_subscribers)
00271             self._remove_topic_menu.addAction(all_action)
00272 
00273         self.remove_topic_button.setMenu(self._remove_topic_menu)
00274 
00275     def add_topic(self, topic_name):
00276         topics_changed = False
00277         for topic_name in get_plot_fields(topic_name)[0]:
00278             if topic_name in self._rosdata:
00279                 qWarning('PlotWidget.add_topic(): topic already subscribed: %s' % topic_name)
00280                 continue
00281             self._rosdata[topic_name] = ROSData(topic_name, self._start_time)
00282             if self._rosdata[topic_name].error is not None:
00283                 qWarning(str(self._rosdata[topic_name].error))
00284                 del self._rosdata[topic_name]
00285             else:
00286                 data_x, data_y = self._rosdata[topic_name].next()
00287                 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00288                 topics_changed = True
00289 
00290         if topics_changed:
00291             self._subscribed_topics_changed()
00292 
00293     def remove_topic(self, topic_name):
00294         self._rosdata[topic_name].close()
00295         del self._rosdata[topic_name]
00296         self.data_plot.remove_curve(topic_name)
00297 
00298         self._subscribed_topics_changed()
00299 
00300     def clear_plot(self):
00301         for topic_name, _ in self._rosdata.items():
00302             self.data_plot.clear_values(topic_name)
00303         self.data_plot.redraw()
00304 
00305     def clean_up_subscribers(self):
00306         for topic_name, rosdata in self._rosdata.items():
00307             rosdata.close()
00308             self.data_plot.remove_curve(topic_name)
00309         self._rosdata = {}
00310 
00311         self._subscribed_topics_changed()
00312 
00313     def enable_timer(self, enabled=True):
00314         if enabled:
00315             self._update_plot_timer.start(self._redraw_interval)
00316         else:
00317             self._update_plot_timer.stop()


rqt_plot
Author(s): Dorian Scholz
autogenerated on Mon May 1 2017 02:41:23