plot_widget.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 # * Neither the name of the TU Darmstadt nor the names of its
17 # contributors may be used to endorse or promote products derived
18 # from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 # POSSIBILITY OF SUCH DAMAGE.
32 
33 import os
34 import rospkg
35 import roslib
36 
37 from python_qt_binding import loadUi
38 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Slot
39 from python_qt_binding.QtGui import QIcon
40 from python_qt_binding.QtWidgets import QAction, QMenu, QWidget
41 
42 import rospy
43 
44 from rqt_py_common.topic_completer import TopicCompleter
45 from rqt_py_common import topic_helpers
46 
47 from . rosplot import ROSData, RosPlotException
48 
49 
50 def get_plot_fields(topic_name):
51  topic_type, real_topic, _ = topic_helpers.get_topic_type(topic_name)
52  if topic_type is None:
53  message = "topic %s does not exist" % (topic_name)
54  return [], message
55  field_name = topic_name[len(real_topic) + 1:]
56 
57  slot_type, is_array, array_size = roslib.msgs.parse_type(topic_type)
58  field_class = roslib.message.get_message_class(slot_type)
59  if field_class is None:
60  message = "type of topic %s is unknown" % (topic_name)
61  return [], message
62 
63  fields = [f for f in field_name.split('/') if f]
64 
65  for field in fields:
66  # parse the field name for an array index
67  try:
68  field, _, field_index = roslib.msgs.parse_type(field)
69  except roslib.msgs.MsgSpecException:
70  message = "invalid field %s in topic %s" % (field, real_topic)
71  return [], message
72 
73  if field not in getattr(field_class, '__slots__', []):
74  message = "no field %s in topic %s" % (field_name, real_topic)
75  return [], message
76  slot_type = field_class._slot_types[field_class.__slots__.index(field)]
77  slot_type, slot_is_array, array_size = roslib.msgs.parse_type(slot_type)
78  is_array = slot_is_array and field_index is None
79 
80  field_class = topic_helpers.get_type_class(slot_type)
81 
82  if field_class in (int, float, bool):
83  topic_kind = 'boolean' if field_class == bool else 'numeric'
84  if is_array:
85  if array_size is not None:
86  message = "topic %s is fixed-size %s array" % (topic_name, topic_kind)
87  return ["%s[%d]" % (topic_name, i) for i in range(array_size)], message
88  else:
89  message = "topic %s is variable-size %s array" % (topic_name, topic_kind)
90  return [], message
91  else:
92  message = "topic %s is %s" % (topic_name, topic_kind)
93  return [topic_name], message
94  else:
95  if not roslib.msgs.is_valid_constant_type(slot_type):
96  numeric_fields = []
97  for i, slot in enumerate(field_class.__slots__):
98  slot_type = field_class._slot_types[i]
99  slot_type, is_array, array_size = roslib.msgs.parse_type(slot_type)
100  slot_class = topic_helpers.get_type_class(slot_type)
101  if slot_class in (int, float) and not is_array:
102  numeric_fields.append(slot)
103  message = ""
104  if len(numeric_fields) > 0:
105  message = "%d plottable fields in %s" % (len(numeric_fields), topic_name)
106  else:
107  message = "No plottable fields in %s" % (topic_name)
108  return ["%s/%s" % (topic_name, f) for f in numeric_fields], message
109  else:
110  message = "Topic %s is not numeric" % (topic_name)
111  return [], message
112 
113 
114 def is_plottable(topic_name):
115  fields, message = get_plot_fields(topic_name)
116  return len(fields) > 0, message
117 
118 
119 class PlotWidget(QWidget):
120  _redraw_interval = 40
121 
122  def __init__(self, initial_topics=None, start_paused=False):
123  super(PlotWidget, self).__init__()
124  self.setObjectName('PlotWidget')
125 
126  self._initial_topics = initial_topics
127 
128  rp = rospkg.RosPack()
129  ui_file = os.path.join(rp.get_path('rqt_plot'), 'resource', 'plot.ui')
130  loadUi(ui_file, self)
131  self.subscribe_topic_button.setIcon(QIcon.fromTheme('list-add'))
132  self.remove_topic_button.setIcon(QIcon.fromTheme('list-remove'))
133  self.pause_button.setIcon(QIcon.fromTheme('media-playback-pause'))
134  self.clear_button.setIcon(QIcon.fromTheme('edit-clear'))
135  self.data_plot = None
136 
137  self.subscribe_topic_button.setEnabled(False)
138  if start_paused:
139  self.pause_button.setChecked(True)
140 
141  self._topic_completer = TopicCompleter(self.topic_edit)
142  self._topic_completer.update_topics()
143  self.topic_edit.setCompleter(self._topic_completer)
144 
145  self._start_time = rospy.get_time()
146  self._rosdata = {}
147  self._remove_topic_menu = QMenu()
148 
149  # init and start update timer for plot
150  self._update_plot_timer = QTimer(self)
151  self._update_plot_timer.timeout.connect(self.update_plot)
152 
153  def switch_data_plot_widget(self, data_plot):
154  self.enable_timer(enabled=False)
155 
156  self.data_plot_layout.removeWidget(self.data_plot)
157  if self.data_plot is not None:
158  self.data_plot.close()
159 
160  self.data_plot = data_plot
161  self.data_plot_layout.addWidget(self.data_plot)
162  self.data_plot.autoscroll(self.autoscroll_checkbox.isChecked())
163 
164  # setup drag 'n drop
165  self.data_plot.dropEvent = self.dropEvent
166  self.data_plot.dragEnterEvent = self.dragEnterEvent
167 
168  if self._initial_topics:
169  for topic_name in self._initial_topics:
170  self.add_topic(topic_name)
171  self._initial_topics = None
172  else:
173  for topic_name, rosdata in self._rosdata.items():
174  data_x, data_y = rosdata.next()
175  self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
176 
178 
179  @Slot('QDragEnterEvent*')
180  def dragEnterEvent(self, event):
181  # get topic name
182  if not event.mimeData().hasText():
183  if not hasattr(event.source(), 'selectedItems') or \
184  len(event.source().selectedItems()) == 0:
185  qWarning(
186  'Plot.dragEnterEvent(): not hasattr(event.source(), selectedItems) or '
187  'len(event.source().selectedItems()) == 0')
188  return
189  item = event.source().selectedItems()[0]
190  topic_name = item.data(0, Qt.UserRole)
191  if topic_name == None:
192  qWarning('Plot.dragEnterEvent(): not hasattr(item, ros_topic_name_)')
193  return
194  else:
195  topic_name = str(event.mimeData().text())
196 
197  # check for plottable field type
198  plottable, message = is_plottable(topic_name)
199  if plottable:
200  event.acceptProposedAction()
201  else:
202  qWarning('Plot.dragEnterEvent(): rejecting: "%s"' % (message))
203 
204  @Slot('QDropEvent*')
205  def dropEvent(self, event):
206  if event.mimeData().hasText():
207  topic_name = str(event.mimeData().text())
208  else:
209  droped_item = event.source().selectedItems()[0]
210  topic_name = str(droped_item.data(0, Qt.UserRole))
211  self.add_topic(topic_name)
212 
213  @Slot(str)
214  def on_topic_edit_textChanged(self, topic_name):
215  # on empty topic name, update topics
216  if topic_name in ('', '/'):
217  self._topic_completer.update_topics()
218 
219  plottable, message = is_plottable(topic_name)
220  self.subscribe_topic_button.setEnabled(plottable)
221  self.subscribe_topic_button.setToolTip(message)
222 
223  @Slot()
225  if self.subscribe_topic_button.isEnabled():
226  self.add_topic(str(self.topic_edit.text()))
227 
228  @Slot()
230  self.add_topic(str(self.topic_edit.text()))
231 
232  @Slot(bool)
233  def on_pause_button_clicked(self, checked):
234  self.enable_timer(not checked)
235 
236  @Slot(bool)
237  def on_autoscroll_checkbox_clicked(self, checked):
238  self.data_plot.autoscroll(checked)
239  if checked:
240  self.data_plot.redraw()
241 
242  @Slot()
244  self.clear_plot()
245 
246  def update_plot(self):
247  if self.data_plot is not None:
248  needs_redraw = False
249  for topic_name, rosdata in self._rosdata.items():
250  try:
251  data_x, data_y = rosdata.next()
252  if data_x or data_y:
253  self.data_plot.update_values(topic_name, data_x, data_y)
254  needs_redraw = True
255  except RosPlotException as e:
256  qWarning('PlotWidget.update_plot(): error in rosplot: %s' % e)
257  if needs_redraw:
258  self.data_plot.redraw()
259 
262  if not self.pause_button.isChecked():
263  # if pause button is not pressed, enable timer based on subscribed topics
264  self.enable_timer(self._rosdata)
265  self.data_plot.redraw()
266 
268  def make_remove_topic_function(x):
269  return lambda: self.remove_topic(x)
270 
271  self._remove_topic_menu.clear()
272  for topic_name in sorted(self._rosdata.keys()):
273  action = QAction(topic_name, self._remove_topic_menu)
274  action.triggered.connect(make_remove_topic_function(topic_name))
275  self._remove_topic_menu.addAction(action)
276 
277  if len(self._rosdata) > 1:
278  all_action = QAction('All', self._remove_topic_menu)
279  all_action.triggered.connect(self.clean_up_subscribers)
280  self._remove_topic_menu.addAction(all_action)
281 
282  self.remove_topic_button.setMenu(self._remove_topic_menu)
283 
284  def add_topic(self, topic_name):
285  topics_changed = False
286  for topic_name in get_plot_fields(topic_name)[0]:
287  if topic_name in self._rosdata:
288  qWarning('PlotWidget.add_topic(): topic already subscribed: %s' % topic_name)
289  continue
290  self._rosdata[topic_name] = ROSData(topic_name, self._start_time)
291  if self._rosdata[topic_name].error is not None:
292  qWarning(str(self._rosdata[topic_name].error))
293  del self._rosdata[topic_name]
294  else:
295  data_x, data_y = self._rosdata[topic_name].next()
296  self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
297  topics_changed = True
298 
299  if topics_changed:
301 
302  def remove_topic(self, topic_name):
303  self._rosdata[topic_name].close()
304  del self._rosdata[topic_name]
305  self.data_plot.remove_curve(topic_name)
306 
308 
309  def clear_plot(self):
310  for topic_name, _ in self._rosdata.items():
311  self.data_plot.clear_values(topic_name)
312  self.data_plot.redraw()
313 
315  for topic_name, rosdata in self._rosdata.items():
316  rosdata.close()
317  self.data_plot.remove_curve(topic_name)
318  self._rosdata = {}
319 
321 
322  def enable_timer(self, enabled=True):
323  if enabled:
324  self._update_plot_timer.start(self._redraw_interval)
325  else:
326  self._update_plot_timer.stop()
def add_topic(self, topic_name)
Definition: plot_widget.py:284
def dragEnterEvent(self, event)
Definition: plot_widget.py:180
def on_pause_button_clicked(self, checked)
Definition: plot_widget.py:233
def __init__(self, initial_topics=None, start_paused=False)
Definition: plot_widget.py:122
def on_topic_edit_textChanged(self, topic_name)
Definition: plot_widget.py:214
def get_plot_fields(topic_name)
Definition: plot_widget.py:50
def remove_topic(self, topic_name)
Definition: plot_widget.py:302
def is_plottable(topic_name)
Definition: plot_widget.py:114
def on_autoscroll_checkbox_clicked(self, checked)
Definition: plot_widget.py:237
def switch_data_plot_widget(self, data_plot)
Definition: plot_widget.py:153
def enable_timer(self, enabled=True)
Definition: plot_widget.py:322


rqt_plot
Author(s): Dorian Scholz
autogenerated on Sun Mar 17 2019 02:21:35