ros_graph.py
Go to the documentation of this file.
1 # Copyright (c) 2011, Dirk Thomas, TU Darmstadt
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions
6 # are met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following
12 # disclaimer in the documentation and/or other materials provided
13 # with the distribution.
14 # * Neither the name of the TU Darmstadt nor the names of its
15 # contributors may be used to endorse or promote products derived
16 # from this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 
31 from __future__ import division
32 import os
33 import rospkg
34 
35 from python_qt_binding import loadUi
36 from python_qt_binding.QtCore import QAbstractListModel, QFile, QIODevice, Qt, Signal
37 from python_qt_binding.QtGui import QIcon, QImage, QPainter
38 from python_qt_binding.QtWidgets import QCompleter, QFileDialog, QGraphicsScene, QWidget
39 from python_qt_binding.QtSvg import QSvgGenerator
40 
41 import rosgraph.impl.graph
42 import rosservice
43 import rostopic
44 
45 from qt_dotgraph.dot_to_qt import DotToQtGenerator
46 # pydot requires some hacks
47 from qt_dotgraph.pydotfactory import PydotFactory
48 from rqt_gui_py.plugin import Plugin
49 # TODO: use pygraphviz instead, but non-deterministic layout will first be resolved in graphviz 2.30
50 # from qtgui_plugin.pygraphvizfactory import PygraphvizFactory
51 
52 from .dotcode import RosGraphDotcodeGenerator, NODE_NODE_GRAPH, NODE_TOPIC_ALL_GRAPH, NODE_TOPIC_GRAPH
53 from .interactive_graphics_view import InteractiveGraphicsView
54 
55 try:
56  unicode
57  # we're on python2, or the "unicode" function has already been defined elsewhere
58 except NameError:
59  unicode = str
60  # we're on python3
61 
62 
63 class RepeatedWordCompleter(QCompleter):
64 
65  """A completer that completes multiple times from a list"""
66 
67  def init(self, parent=None):
68  QCompleter.init(self, parent)
69 
70  def pathFromIndex(self, index):
71  path = QCompleter.pathFromIndex(self, index)
72  lst = unicode(self.widget().text()).split(',')
73  if len(lst) > 1:
74  path = '%s, %s' % (','.join(lst[:-1]), path)
75  return path
76 
77  def splitPath(self, path):
78  path = unicode(path.split(',')[-1]).lstrip(' ')
79  return [path]
80 
81 
82 class NamespaceCompletionModel(QAbstractListModel):
83 
84  """Ros package and stacknames"""
85 
86  def __init__(self, linewidget, topics_only):
87  super(NamespaceCompletionModel, self).__init__(linewidget)
88  self.names = []
89 
90  def refresh(self, names):
91  namesset = set()
92  for n in names:
93  namesset.add(unicode(n).strip())
94  namesset.add("-%s" % (unicode(n).strip()))
95  self.names = sorted(namesset)
96 
97  def rowCount(self, parent):
98  return len(self.names)
99 
100  def data(self, index, role):
101  if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
102  return self.names[index.row()]
103  return None
104 
105 
107 
108  _deferred_fit_in_view = Signal()
109 
110  def __init__(self, context):
111  super(RosGraph, self).__init__(context)
112  self.initialized = False
113  self.setObjectName('RosGraph')
114 
115  self._graph = None
116  self._current_dotcode = None
117 
118  self._widget = QWidget()
119 
120  # factory builds generic dotcode items
122  # self.dotcode_factory = PygraphvizFactory()
123  # generator builds rosgraph
124  self.dotcode_generator = RosGraphDotcodeGenerator()
125  # dot_to_qt transforms into Qt elements using dot layout
127 
128  rp = rospkg.RosPack()
129  ui_file = os.path.join(rp.get_path('rqt_graph'), 'resource', 'RosGraph.ui')
130  loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
131  self._widget.setObjectName('RosGraphUi')
132  if context.serial_number() > 1:
133  self._widget.setWindowTitle(
134  self._widget.windowTitle() + (' (%d)' % context.serial_number()))
135 
136  self._scene = QGraphicsScene()
137  self._scene.setBackgroundBrush(Qt.white)
138  self._widget.graphics_view.setScene(self._scene)
139 
140  self._widget.graph_type_combo_box.insertItem(0, self.tr('Nodes only'), NODE_NODE_GRAPH)
141  self._widget.graph_type_combo_box.insertItem(
142  1, self.tr('Nodes/Topics (active)'), NODE_TOPIC_GRAPH)
143  self._widget.graph_type_combo_box.insertItem(
144  2, self.tr('Nodes/Topics (all)'), NODE_TOPIC_ALL_GRAPH)
145  self._widget.graph_type_combo_box.setCurrentIndex(0)
146  self._widget.graph_type_combo_box.currentIndexChanged.connect(self._refresh_rosgraph)
147 
148  self.node_completionmodel = NamespaceCompletionModel(self._widget.filter_line_edit, False)
149  completer = RepeatedWordCompleter(self.node_completionmodel, self)
150  completer.setCompletionMode(QCompleter.PopupCompletion)
151  completer.setWrapAround(True)
152  completer.setCaseSensitivity(Qt.CaseInsensitive)
153  self._widget.filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
154  self._widget.filter_line_edit.setCompleter(completer)
155 
157  self._widget.topic_filter_line_edit, False)
158  topic_completer = RepeatedWordCompleter(self.topic_completionmodel, self)
159  topic_completer.setCompletionMode(QCompleter.PopupCompletion)
160  topic_completer.setWrapAround(True)
161  topic_completer.setCaseSensitivity(Qt.CaseInsensitive)
162  self._widget.topic_filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
163  self._widget.topic_filter_line_edit.setCompleter(topic_completer)
164 
165  self._widget.namespace_cluster_spin_box.valueChanged.connect(self._refresh_rosgraph)
166  self._widget.actionlib_check_box.clicked.connect(self._refresh_rosgraph)
167  self._widget.dead_sinks_check_box.clicked.connect(self._refresh_rosgraph)
168  self._widget.leaf_topics_check_box.clicked.connect(self._refresh_rosgraph)
169  self._widget.quiet_check_box.clicked.connect(self._refresh_rosgraph)
170  self._widget.unreachable_check_box.clicked.connect(self._refresh_rosgraph)
171  self._widget.group_tf_check_box.clicked.connect(self._refresh_rosgraph)
172  self._widget.hide_tf_nodes_check_box.clicked.connect(self._refresh_rosgraph)
173  self._widget.group_image_check_box.clicked.connect(self._refresh_rosgraph)
174 
175  self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
176  self._widget.refresh_graph_push_button.pressed.connect(self._update_rosgraph)
177 
178  self._widget.highlight_connections_check_box.toggled.connect(self._redraw_graph_view)
179  self._widget.auto_fit_graph_check_box.toggled.connect(self._redraw_graph_view)
180  self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
181  self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
182 
183  self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
184  self._widget.load_dot_push_button.pressed.connect(self._load_dot)
185  self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
186  self._widget.save_dot_push_button.pressed.connect(self._save_dot)
187  self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
188  self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
189  self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
190  self._widget.save_as_image_push_button.pressed.connect(self._save_image)
191 
192  self._update_rosgraph()
193  self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
194  self._deferred_fit_in_view.emit()
195 
196  context.add_widget(self._widget)
197 
198  def save_settings(self, plugin_settings, instance_settings):
199  instance_settings.set_value(
200  'graph_type_combo_box_index', self._widget.graph_type_combo_box.currentIndex())
201  instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
202  instance_settings.set_value(
203  'topic_filter_line_edit_text', self._widget.topic_filter_line_edit.text())
204  instance_settings.set_value(
205  'namespace_cluster_spin_box_value', self._widget.namespace_cluster_spin_box.value())
206  instance_settings.set_value(
207  'actionlib_check_box_state', self._widget.actionlib_check_box.isChecked())
208  instance_settings.set_value(
209  'dead_sinks_check_box_state', self._widget.dead_sinks_check_box.isChecked())
210  instance_settings.set_value(
211  'leaf_topics_check_box_state', self._widget.leaf_topics_check_box.isChecked())
212  instance_settings.set_value(
213  'quiet_check_box_state', self._widget.quiet_check_box.isChecked())
214  instance_settings.set_value(
215  'unreachable_check_box_state', self._widget.unreachable_check_box.isChecked())
216  instance_settings.set_value(
217  'auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
218  instance_settings.set_value(
219  'highlight_connections_check_box_state', self._widget.highlight_connections_check_box.isChecked())
220  instance_settings.set_value(
221  'group_tf_check_box_state', self._widget.group_tf_check_box.isChecked())
222  instance_settings.set_value(
223  'hide_tf_nodes_check_box_state', self._widget.hide_tf_nodes_check_box.isChecked())
224  instance_settings.set_value(
225  'group_image_check_box_state', self._widget.group_image_check_box.isChecked())
226  instance_settings.set_value(
227  'hide_dynamic_reconfigure_check_box_state', self._widget.hide_dynamic_reconfigure_check_box.isChecked())
228 
229  def restore_settings(self, plugin_settings, instance_settings):
230  self._widget.graph_type_combo_box.setCurrentIndex(
231  int(instance_settings.value('graph_type_combo_box_index', 0)))
232  self._widget.filter_line_edit.setText(instance_settings.value('filter_line_edit_text', '/'))
233  self._widget.topic_filter_line_edit.setText(
234  instance_settings.value('topic_filter_line_edit_text', '/'))
235  self._widget.namespace_cluster_spin_box.setValue(
236  int(instance_settings.value('namespace_cluster_spin_box_value', 2)))
237  self._widget.actionlib_check_box.setChecked(
238  instance_settings.value('actionlib_check_box_state', True) in [True, 'true'])
239  self._widget.dead_sinks_check_box.setChecked(
240  instance_settings.value('dead_sinks_check_box_state', True) in [True, 'true'])
241  self._widget.leaf_topics_check_box.setChecked(
242  instance_settings.value('leaf_topics_check_box_state', True) in [True, 'true'])
243  self._widget.quiet_check_box.setChecked(
244  instance_settings.value('quiet_check_box_state', True) in [True, 'true'])
245  self._widget.unreachable_check_box.setChecked(
246  instance_settings.value('unreachable_check_box_state', True) in [True, 'true'])
247  self._widget.auto_fit_graph_check_box.setChecked(
248  instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
249  self._widget.highlight_connections_check_box.setChecked(
250  instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
251  self._widget.hide_tf_nodes_check_box.setChecked(
252  instance_settings.value('hide_tf_nodes_check_box_state', False) in [True, 'true'])
253  self._widget.group_tf_check_box.setChecked(
254  instance_settings.value('group_tf_check_box_state', True) in [True, 'true'])
255  self._widget.group_image_check_box.setChecked(
256  instance_settings.value('group_image_check_box_state', True) in [True, 'true'])
257  self._widget.hide_dynamic_reconfigure_check_box.setChecked(
258  instance_settings.value('hide_dynamic_reconfigure_check_box_state', True) in [True, 'true'])
259  self.initialized = True
260  self._refresh_rosgraph()
261 
262  def _update_rosgraph(self):
263  # re-enable controls customizing fetched ROS graph
264  self._widget.graph_type_combo_box.setEnabled(True)
265  self._widget.filter_line_edit.setEnabled(True)
266  self._widget.topic_filter_line_edit.setEnabled(True)
267  self._widget.namespace_cluster_spin_box.setEnabled(True)
268  self._widget.actionlib_check_box.setEnabled(True)
269  self._widget.dead_sinks_check_box.setEnabled(True)
270  self._widget.leaf_topics_check_box.setEnabled(True)
271  self._widget.quiet_check_box.setEnabled(True)
272  self._widget.unreachable_check_box.setEnabled(True)
273  self._widget.group_tf_check_box.setEnabled(True)
274  self._widget.hide_tf_nodes_check_box.setEnabled(True)
275  self._widget.group_image_check_box.setEnabled(True)
276  self._widget.hide_dynamic_reconfigure_check_box.setEnabled(True)
277 
278  self._graph = rosgraph.impl.graph.Graph()
279  self._graph.set_master_stale(5.0)
280  self._graph.set_node_stale(5.0)
281  self._graph.update()
282  self.node_completionmodel.refresh(self._graph.nn_nodes)
283  self.topic_completionmodel.refresh(self._graph.nt_nodes)
284  self._refresh_rosgraph()
285 
286  def _refresh_rosgraph(self):
287  if not self.initialized:
288  return
290 
291  def _generate_dotcode(self):
292  ns_filter = self._widget.filter_line_edit.text()
293  topic_filter = self._widget.topic_filter_line_edit.text()
294  graph_mode = self._widget.graph_type_combo_box.itemData(
295  self._widget.graph_type_combo_box.currentIndex())
296  orientation = 'LR'
297  namespace_cluster = self._widget.namespace_cluster_spin_box.value()
298  accumulate_actions = self._widget.actionlib_check_box.isChecked()
299  hide_dead_end_topics = self._widget.dead_sinks_check_box.isChecked()
300  hide_single_connection_topics = self._widget.leaf_topics_check_box.isChecked()
301  quiet = self._widget.quiet_check_box.isChecked()
302  unreachable = self._widget.unreachable_check_box.isChecked()
303  group_tf_nodes = self._widget.group_tf_check_box.isChecked()
304  hide_tf_nodes = self._widget.hide_tf_nodes_check_box.isChecked()
305  group_image_nodes = self._widget.group_image_check_box.isChecked()
306  hide_dynamic_reconfigure = self._widget.hide_dynamic_reconfigure_check_box.isChecked()
307 
308  return self.dotcode_generator.generate_dotcode(
309  rosgraphinst=self._graph,
310  ns_filter=ns_filter,
311  topic_filter=topic_filter,
312  graph_mode=graph_mode,
313  hide_single_connection_topics=hide_single_connection_topics,
314  hide_dead_end_topics=hide_dead_end_topics,
315  cluster_namespaces_level=namespace_cluster,
316  accumulate_actions=accumulate_actions,
317  dotcode_factory=self.dotcode_factory,
318  orientation=orientation,
319  quiet=quiet,
320  unreachable=unreachable,
321  group_tf_nodes=group_tf_nodes,
322  hide_tf_nodes=hide_tf_nodes,
323  group_image_nodes=group_image_nodes,
324  hide_dynamic_reconfigure=hide_dynamic_reconfigure)
325 
326  def _update_graph_view(self, dotcode):
327  if dotcode == self._current_dotcode:
328  return
329  self._current_dotcode = dotcode
330  self._redraw_graph_view()
331 
332  def _generate_tool_tip(self, url):
333  if url is not None and ':' in url:
334  item_type, item_path = url.split(':', 1)
335  if item_type == 'node':
336  tool_tip = 'Node:\n %s' % (item_path)
337  service_names = rosservice.get_service_list(node=item_path)
338  if service_names:
339  tool_tip += '\nServices:'
340  for service_name in service_names:
341  try:
342  service_type = rosservice.get_service_type(service_name)
343  tool_tip += '\n %s [%s]' % (service_name, service_type)
344  except rosservice.ROSServiceIOException as e:
345  tool_tip += '\n %s' % (e)
346  return tool_tip
347  elif item_type == 'topic':
348  topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
349  return 'Topic:\n %s\nType:\n %s' % (topic_name, topic_type)
350  return url
351 
353  self._scene.clear()
354 
355  if self._widget.highlight_connections_check_box.isChecked():
356  highlight_level = 3
357  else:
358  highlight_level = 1
359 
360  # layout graph and create qt items
361  (nodes, edges) = self.dot_to_qt.dotcode_to_qt_items(self._current_dotcode,
362  highlight_level=highlight_level,
363  same_label_siblings=True,
364  scene=self._scene)
365 
366  self._scene.setSceneRect(self._scene.itemsBoundingRect())
367  if self._widget.auto_fit_graph_check_box.isChecked():
368  self._fit_in_view()
369 
370  def _load_dot(self, file_name=None):
371  if file_name is None:
372  file_name, _ = QFileDialog.getOpenFileName(
373  self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
374  if file_name is None or file_name == '':
375  return
376 
377  try:
378  fh = open(file_name, 'rb')
379  dotcode = fh.read()
380  fh.close()
381  except IOError:
382  return
383 
384  # disable controls customizing fetched ROS graph
385  self._widget.graph_type_combo_box.setEnabled(False)
386  self._widget.filter_line_edit.setEnabled(False)
387  self._widget.topic_filter_line_edit.setEnabled(False)
388  self._widget.namespace_cluster_spin_box.setEnabled(False)
389  self._widget.actionlib_check_box.setEnabled(False)
390  self._widget.dead_sinks_check_box.setEnabled(False)
391  self._widget.leaf_topics_check_box.setEnabled(False)
392  self._widget.quiet_check_box.setEnabled(False)
393  self._widget.unreachable_check_box.setEnabled(False)
394  self._widget.group_tf_check_box.setEnabled(False)
395  self._widget.hide_tf_nodes_check_box.setEnabled(False)
396  self._widget.group_image_check_box.setEnabled(False)
397  self._widget.hide_dynamic_reconfigure_check_box.setEnabled(False)
398 
399  self._update_graph_view(dotcode)
400 
401  def _fit_in_view(self):
402  self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
403 
404  def _save_dot(self):
405  file_name, _ = QFileDialog.getSaveFileName(
406  self._widget, self.tr('Save as DOT'), 'rosgraph.dot', self.tr('DOT graph (*.dot)'))
407  if file_name is None or file_name == '':
408  return
409 
410  handle = QFile(file_name)
411  if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
412  return
413 
414  handle.write(self._current_dotcode)
415  handle.close()
416 
417  def _save_svg(self):
418  file_name, _ = QFileDialog.getSaveFileName(
419  self._widget, self.tr('Save as SVG'), 'rosgraph.svg', self.tr('Scalable Vector Graphic (*.svg)'))
420  if file_name is None or file_name == '':
421  return
422 
423  generator = QSvgGenerator()
424  generator.setFileName(file_name)
425  generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
426 
427  painter = QPainter(generator)
428  painter.setRenderHint(QPainter.Antialiasing)
429  self._scene.render(painter)
430  painter.end()
431 
432  def _save_image(self):
433  file_name, _ = QFileDialog.getSaveFileName(
434  self._widget, self.tr('Save as image'), 'rosgraph.png', self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
435  if file_name is None or file_name == '':
436  return
437 
438  img = QImage((self._scene.sceneRect().size() * 2.0)
439  .toSize(), QImage.Format_ARGB32_Premultiplied)
440  painter = QPainter(img)
441  painter.setRenderHint(QPainter.Antialiasing)
442  self._scene.render(painter)
443  painter.end()
444  img.save(file_name)
def __init__(self, linewidget, topics_only)
Definition: ros_graph.py:86
def restore_settings(self, plugin_settings, instance_settings)
Definition: ros_graph.py:229
def __init__(self, context)
Definition: ros_graph.py:110
def save_settings(self, plugin_settings, instance_settings)
Definition: ros_graph.py:198
def _generate_tool_tip(self, url)
Definition: ros_graph.py:332
def _load_dot(self, file_name=None)
Definition: ros_graph.py:370
def _update_graph_view(self, dotcode)
Definition: ros_graph.py:326


rqt_graph
Author(s): Dirk Thomas , Aaron Blasdel
autogenerated on Mon Oct 19 2020 03:41:30